From 968fb1f9432f6ac2a2bc7e34c0f28120d491522f Mon Sep 17 00:00:00 2001 From: Marius Stanciu Date: Sat, 17 Jun 2023 22:30:17 +0300 Subject: [PATCH] - NCC Plugin: modified the previous change and now the simplification action is much bigger reducing the number of coordinates by a factor of 20 (20 times less) - Paint Plugin: same as above - Ncc Plugin: added some more tooltips - Isolation Plugin: fixed some issues when using the Rest Machining option --- CHANGELOG.md | 9 +- appPlugins/ToolIsolation.py | 34 +++---- appPlugins/ToolNCC.py | 184 ++++++++++++++++++++---------------- appPlugins/ToolPaint.py | 24 +++-- appProcess.py | 7 +- camlib.py | 14 ++- 6 files changed, 159 insertions(+), 113 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b2feb7fd..30363cf2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,9 +7,16 @@ CHANGELOG for FlatCAM Evo beta ================================================= +17.06.2023 + +- NCC Plugin: modified the previous change and now the simplification action is much bigger reducing the number of coordinates by a factor of 20 (20 times less) +- Paint Plugin: same as above +- Ncc Plugin: added some more tooltips +- Isolation Plugin: fixed some issues when using the Rest Machining option + 16.06.2023 -- NCC Tool: made a small optimization by adding a simplification in the clearing geometry +- NCC Plugin: made a small optimization by adding a simplification in the clearing geometry 15.06.2023 diff --git a/appPlugins/ToolIsolation.py b/appPlugins/ToolIsolation.py index b860f686..a720eb18 100644 --- a/appPlugins/ToolIsolation.py +++ b/appPlugins/ToolIsolation.py @@ -2074,13 +2074,13 @@ class ToolIsolation(AppTool, Gerber): solid_geo = self.area_intersection(solid_geo, intersection_geo=lim_area) # make sure that no empty geometry element is in the solid_geometry - new_solid_geo = [geo for geo in solid_geo if not geo.is_empty] + new_solid_geo = flatten_shapely_geometry(solid_geo) tools_storage.update({ tool: { 'tooldia': float(tool_dia), 'data': tool_data, - 'solid_geometry': deepcopy(new_solid_geo) + 'solid_geometry': new_solid_geo } }) tools_storage[tool]['data']['tools_mill_tooldia'] = float(tool_dia) @@ -2090,11 +2090,10 @@ class ToolIsolation(AppTool, Gerber): if not work_geo: break - total_solid_geometry = self.flatten_list(total_solid_geometry) - total_solid_geometry = [g for g in total_solid_geometry if not g.is_empty] if simp_en: - total_solid_geometry = [ - g.simplify(tolerance=simp_tol) for g in total_solid_geometry if not g.is_empty] + total_solid_geometry = flatten_shapely_geometry(total_solid_geometry, simplify_tolerance=simp_tol) + else: + total_solid_geometry = flatten_shapely_geometry(total_solid_geometry) # clean the progressive plotted shapes if it was used if plot and self.app.options["tools_iso_plotting"] == 'progressive': @@ -2169,7 +2168,7 @@ class ToolIsolation(AppTool, Gerber): pt = geo.representative_point() coords = '(%s, %s), ' % (str(pt.x), str(pt.y)) msg += coords - self.app.inform_shell.emit(msg=msg) + self.app.inform_shell.emit(msg) def combined_normal(self, iso_obj, iso2geo, tools_storage, lim_area, sel_tools, iso_except, extra_passes=None, negative_dia=None, simp_en=False, simp_tol=0.001, plot=True, prog_plot=None): @@ -3266,30 +3265,21 @@ class ToolIsolation(AppTool, Gerber): else: not_isolated_geo.append(geo) - work_geo_shp = work_geo.geoms if isinstance(work_geo, MultiPolygon) else work_geo + work_geo_shp = flatten_shapely_geometry(work_geo) if invert: try: pl = [] for p in work_geo_shp: - if p is not None: - if isinstance(p, Polygon): - pl.append(Polygon(p.exterior.coords[::-1], p.interiors)) - elif isinstance(p, LinearRing): - pl.append(Polygon(p.coords[::-1])) + if isinstance(p, Polygon): + pl.append(Polygon(p.exterior.coords[::-1], p.interiors)) + elif isinstance(p, LinearRing): + pl.append(Polygon(p.coords[::-1])) work_geo_shp = MultiPolygon(pl) - except TypeError: - if isinstance(work_geo_shp, Polygon) and work_geo_shp is not None: - work_geo_shp = [Polygon(work_geo_shp.exterior.coords[::-1], work_geo_shp.interiors)] - elif isinstance(work_geo_shp, LinearRing) and work_geo_shp is not None: - work_geo_shp = [Polygon(work_geo_shp.coords[::-1])] - else: - self.app.log.debug( - "ToolIsolation.generate_rest_geometry() Error --> Unexpected Geometry %s" % type(work_geo)) except Exception as e: self.app.log.error("ToolIsolation.generate_rest_geometry() Error --> %s" % str(e)) return 'fail', 'fail' - actual_geo = work_geo_shp.geoms if isinstance(work_geo, MultiPolygon) else work_geo_shp + actual_geo = flatten_shapely_geometry(work_geo_shp) if env_iso_type == 0: # exterior for geo in actual_geo: isolated_geo.append(geo.exterior) diff --git a/appPlugins/ToolNCC.py b/appPlugins/ToolNCC.py index 0b727efc..9288efac 100644 --- a/appPlugins/ToolNCC.py +++ b/appPlugins/ToolNCC.py @@ -2187,7 +2187,6 @@ class NonCopperClear(AppTool, Gerber): steps_per_circle=self.circle_steps, overlap=ncc_overlap, contour=ncc_contour, connect=ncc_connect, - simplify_tol=simplify_tol, prog_plot=prog_plot) except grace: return "fail" @@ -2248,8 +2247,10 @@ class NonCopperClear(AppTool, Gerber): self.app.log.error("NonCopperClear.clear_polygon_worker() Combo --> %s" % str(ee)) if cp and cp.objects: - ret_val = list(cp.get_objects()) - return ret_val + if simplify_tol > 0.0: + return [x.simplify(simplify_tol) for x in cp.get_objects()] + else: + return [x for x in cp.get_objects()] else: pt = pol.representative_point() coords = (pt.x, pt.y) @@ -2285,9 +2286,9 @@ class NonCopperClear(AppTool, Gerber): self.app.log.debug("Executing the clear_copper handler ...") if run_threaded: - proc = self.app.proc_container.new('%s...' % _("Non-Copper Clearing")) + proc = self.app.proc_container.new('%s...' % _("Working")) else: - self.app.proc_container.view.set_busy('%s...' % _("Non-Copper Clearing")) + self.app.proc_container.view.set_busy('%s...' % _("Working")) QtWidgets.QApplication.processEvents() # ###################################################################################################### @@ -2300,7 +2301,7 @@ class NonCopperClear(AppTool, Gerber): rest_machining_choice = self.ui.ncc_rest_cb.get_value() # TODO this should be in preferences and in the UI - simplification_value = 0.02 + simplification_value = 0.01 # determine if to use the progressive plotting prog_plot = True if self.app.options["tools_ncc_plotting"] == 'progressive' else False @@ -2360,7 +2361,9 @@ class NonCopperClear(AppTool, Gerber): bbox = self.apply_margin_to_bounding_box(bbox=bbox_geo, box_kind=bbox_kind, ncc_select=ncc_select, ncc_margin=ncc_margin) + # ---------------------------------------------------- # COPPER CLEARING with tools marked for CLEAR# + # ---------------------------------------------------- for tool in sorted_clear_tools: self.app.log.debug("Starting geometry processing for tool: %s" % str(tool)) if self.app.abort_flag: @@ -2376,16 +2379,23 @@ class NonCopperClear(AppTool, Gerber): ) app_obj.proc_container.update_view_text(' %d%%' % 0) + # ---------------------------------------------------- # store here the geometry generated by clear operation + # ---------------------------------------------------- cleared_geo = [] - tool_uid = 0 # find the current tool_uid + # ---------------------------------------------------- + # find the current tool_uid + # ---------------------------------------------------- + tool_uid = 0 for k, v in self.ncc_tools.items(): if float('%.*f' % (self.decimals, v['tooldia'])) == float('%.*f' % (self.decimals, tool)): tool_uid = int(k) break + # ---------------------------------------------------- # parameters that are particular to the current tool + # ---------------------------------------------------- ncc_overlap = float(self.ncc_tools[tool_uid]["data"]["tools_ncc_overlap"]) / 100.0 ncc_method = self.ncc_tools[tool_uid]["data"]["tools_ncc_method"] ncc_connect = self.ncc_tools[tool_uid]["data"]["tools_ncc_connect"] @@ -2393,107 +2403,119 @@ class NonCopperClear(AppTool, Gerber): has_offset = self.ncc_tools[tool_uid]["data"]["tools_ncc_offset_choice"] ncc_offset = float(self.ncc_tools[tool_uid]["data"]["tools_ncc_offset_value"]) + # ---------------------------------------------------- # Area to clear + # ---------------------------------------------------- result = self.get_tool_empty_area(name=name, ncc_obj=ncc_obj, geo_obj=geo_obj, isotooldia=isotd_list, ncc_margin=ncc_margin, has_offset=has_offset, ncc_offset=ncc_offset, tools_storage=tools_storage, bounding_box=bbox) - area, warning_flag = result if area == "fail": self.app.log.debug("Failed to create empty area for this tool.") continue - # Transform area to MultiPolygon - if isinstance(area, Polygon): - area = MultiPolygon([area]) + tool_empty_area = flatten_shapely_geometry(area) + if not tool_empty_area: + continue # variables to display the percentage of work done - geo_len = len(area.geoms) - old_disp_number = 0 + geo_len = len(tool_empty_area) self.app.log.warning("Total number of polygons to be cleared. %s" % str(geo_len)) - tool_empty_area = flatten_shapely_geometry(area.geoms) + # ---------------------------------------------------- + # Copper-clear the Polygons in the non-copper-area + # Iterate over them + # ---------------------------------------------------- + pol_nr = 0 + for p in tool_empty_area: + # provide the app with a way to process the GUI events when in a blocking loop + if not run_threaded: + QtWidgets.QApplication.processEvents() - if tool_empty_area: - pol_nr = 0 - for p in tool_empty_area: + if self.app.abort_flag: + # graceful abort requested by the user + raise grace + + # ---------------------------------------------------- + # attempt to fix possible problems with the polygon + # ---------------------------------------------------- + p = p.buffer(0.0000001) + p = flatten_shapely_geometry(p, simplify_tolerance=simplification_value) + + poly_failed = 0 + for pol in p: # provide the app with a way to process the GUI events when in a blocking loop - if not run_threaded: - QtWidgets.QApplication.processEvents() + QtWidgets.QApplication.processEvents() - if self.app.abort_flag: - # graceful abort requested by the user - raise grace - - # clean the polygon - p = p.buffer(0.0000001) - p = flatten_shapely_geometry(p, simplify_tolerance=simplification_value) - - poly_failed = 0 - for pol in p: - # provide the app with a way to process the GUI events when in a blocking loop - QtWidgets.QApplication.processEvents() - - if pol is not None and pol.is_valid and isinstance(pol, Polygon): - res = self.clear_polygon_worker(pol=pol, tooldia=tool, - ncc_method=ncc_method, - ncc_overlap=ncc_overlap, - ncc_connect=ncc_connect, - ncc_contour=ncc_contour, - simplify_tol=simplification_value, - prog_plot=prog_plot) - if res is not None: - cleared_geo += res - else: - poly_failed += 1 + if pol is not None and pol.is_valid and isinstance(pol, Polygon): + # ---------------------------------------------------- + # This is where copper clearing is happening + # ---------------------------------------------------- + res = self.clear_polygon_worker(pol=pol, tooldia=tool, + ncc_method=ncc_method, + ncc_overlap=ncc_overlap, + ncc_connect=ncc_connect, + ncc_contour=ncc_contour, + simplify_tol=simplification_value, + prog_plot=prog_plot) + if res is not None: + cleared_geo += res else: - self.app.log.warning( - "Expected geo is a Polygon. Instead got a %s" % str(type(pol))) + poly_failed += 1 + else: + self.app.log.warning( + "Expected geo is a Polygon. Instead got a %s" % str(type(pol))) - pol_nr += 1 - disp_number = int(np.interp(pol_nr, [0, geo_len], [0, 100])) - # log.debug("Polygons cleared: %d" % pol_nr) + 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(' %d%%' % disp_number) + old_disp_number = disp_number - if old_disp_number < disp_number <= 100: - self.app.proc_container.update_view_text(' %d%%' % disp_number) - old_disp_number = disp_number - # log.debug("Polygons cleared: %d. Percentage done: %d%%" % (pol_nr, disp_number)) + if poly_failed > 0: + app_obj.poly_not_cleared = True - if poly_failed > 0: - app_obj.poly_not_cleared = True + # --------------------------------------------------------- + # Debug message regarding how many points are in the result + # --------------------------------------------------------- + l_coords = 0 + for i in range(len(cleared_geo)): + l_coords += len(cleared_geo[i].coords) + self.app.log.debug( + "NCC Tool.clear_copper.gen_clear_area() -> Number of cleared geo coords: %s" % str(l_coords)) - l_coords = 0 - for i in range(len(cleared_geo)): - l_coords += len(cleared_geo[i].coords) - self.app.log.debug( - "NCC Tool.clear_copper.gen_clear_area() -> Number of cleared geo coords: %s" % str(l_coords)) + # ----------------------------------------------------------- + # check if there is a geometry at all in the cleared geometry + # ----------------------------------------------------------- + if cleared_geo: + formatted_tool = self.app.dec_format(tool, self.decimals) + # find the tooluid associated with the current tool_dia so we know where to add the tool + # solid_geometry + for k, v in tools_storage.items(): + if self.app.dec_format(v['tooldia'], self.decimals) == formatted_tool: + current_uid = int(k) - # check if there is a geometry at all in the cleared geometry - if cleared_geo: - formatted_tool = self.app.dec_format(tool, self.decimals) - # find the tooluid associated with the current tool_dia so we know where to add the tool - # solid_geometry - for k, v in tools_storage.items(): - if self.app.dec_format(v['tooldia'], self.decimals) == formatted_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 - geo_obj.tools[current_uid] = dict(tools_storage[current_uid]) - break - else: - self.app.log.debug("There are no geometries in the cleared polygon.") + # 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 + geo_obj.tools[current_uid] = dict(tools_storage[current_uid]) + break + else: + self.app.log.debug("There are no geometries in the cleared polygon.") + # ---------------------------------------------------- # clean the progressive plotted shapes if it was used + # ---------------------------------------------------- if self.app.options["tools_ncc_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, uid_val in list(tools_storage.items()): try: # if the solid_geometry (type=list) is empty @@ -2516,7 +2538,9 @@ class NonCopperClear(AppTool, Gerber): geo_obj.multigeo = True geo_obj.tools = dict(tools_storage) + # ------------------------------------------------------------------------------------------------- # test if at least one tool has solid_geometry. If no tool has solid_geometry we raise an Exception + # ------------------------------------------------------------------------------------------------- has_solid_geo = 0 for tid in geo_obj.tools: if geo_obj.tools[tid]['solid_geometry']: @@ -2528,8 +2552,10 @@ class NonCopperClear(AppTool, Gerber): app_obj.inform.emit(msg) return 'fail' + # ---------------------------------------------------------------- # check to see if geo_obj.tools is empty # it will be updated only if there is a solid_geometry for tools + # ---------------------------------------------------------------- if geo_obj.tools: if warning_flag == 0: self.app.inform.emit('[success] %s' % _("NCC Tool clear all done.")) @@ -2889,9 +2915,9 @@ class NonCopperClear(AppTool, Gerber): :return: """ if run_threaded: - proc = self.app.proc_container.new('%s...' % _("Non-Copper Clearing")) + proc = self.app.proc_container.new('%s...' % _("Working")) else: - self.app.proc_container.view.set_busy('%s...' % _("Non-Copper Clearing")) + self.app.proc_container.view.set_busy('%s...' % _("Working")) QtWidgets.QApplication.processEvents() # ##################################################################### diff --git a/appPlugins/ToolPaint.py b/appPlugins/ToolPaint.py index 41605877..94a12eb5 100644 --- a/appPlugins/ToolPaint.py +++ b/appPlugins/ToolPaint.py @@ -1871,6 +1871,9 @@ class ToolPaint(AppTool, Gerber): tools_storage = self.paint_tools if tools_storage is None else tools_storage use_rest_strategy = rest if rest is not None else self.ui.rest_cb.get_value() + # TODO this should be in preferences and in the UI + simplification_value = 0.01 + sorted_tools = [] if tooldia is not None: try: @@ -1959,7 +1962,7 @@ class ToolPaint(AppTool, Gerber): # effective polygon clearing job # ----------------------------- try: - cp = [] + cp_list = [] for pp in poly_buf: # provide the app with a way to process the GUI events when in a blocking loop QtWidgets.QApplication.processEvents() @@ -1970,7 +1973,7 @@ class ToolPaint(AppTool, Gerber): cont=cont, paint_method=paint_method, obj=obj, prog_plot=prog_plot) if geo_res: - cp.append(geo_res) + cp_list.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) @@ -1980,9 +1983,12 @@ class ToolPaint(AppTool, Gerber): old_disp_number = disp_number total_geometry = [] - if cp: - for x in cp: - total_geometry += list(x.get_objects()) + if cp_list: + for cp in cp_list: + if simplification_value > 0.0: + total_geometry += [x.simplify(simplification_value) for x in cp.get_objects()] + else: + total_geometry += [x for x in cp.get_objects()] # clean the geometry total_geometry = [g for g in total_geometry if g and not g.is_empty] @@ -2141,7 +2147,12 @@ class ToolPaint(AppTool, Gerber): 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()) + + if simplification_value > 0.0: + geo_elems = [x.simplify(simplification_value) for x in geo_res.get_objects()] + else: + geo_elems = [x for x in geo_res.get_objects()] + # See if the polygon was completely cleared pp_cleared = unary_union(geo_elems).buffer(tool_dia / 2.0) rest_geo = pp.difference(pp_cleared) @@ -2153,6 +2164,7 @@ class ToolPaint(AppTool, Gerber): 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) diff --git a/appProcess.py b/appProcess.py index f0be2ef5..e328e703 100644 --- a/appProcess.py +++ b/appProcess.py @@ -171,8 +171,11 @@ class FCVisibleProcessContainer(QtCore.QObject, FCProcessContainer): else: self.view.set_busy("%d %s" % (len(self.procs), _("processes running."))) - def update_view_text(self, new_text): + def update_view_text(self, new_text, clear=False): # this has to be called after the method 'new' inherited by this class is called with a string text as param self.new_text = new_text if len(self.procs) == 1: - self.view.set_busy(self.text_to_display_in_activity + self.new_text, no_movie=True) + if clear is False: + self.view.set_busy(self.text_to_display_in_activity + self.new_text, no_movie=True) + else: + self.view.set_busy(self.new_text, no_movie=True) diff --git a/camlib.py b/camlib.py index 21883de9..b9274c6c 100644 --- a/camlib.py +++ b/camlib.py @@ -1425,7 +1425,7 @@ class Geometry(object): return boundary.difference(self.solid_geometry) def clear_polygon_shrink(self, polygon, tooldia, steps_per_circle, overlap=0.15, connect=True, contour=True, - simplify_tol=0.0, prog_plot=False): + prog_plot=False): """ Creates geometry inside a polygon for a tool to cover the whole area. @@ -1474,7 +1474,7 @@ class Geometry(object): QtWidgets.QApplication.processEvents() cl_pol = cl_pol.buffer(-tooldia * (1 - overlap), int(steps_per_circle)) - cl_pol_list = flatten_shapely_geometry(cl_pol, simplify_tolerance=simplify_tol) + cl_pol_list = flatten_shapely_geometry(cl_pol) added_flag = False for tiny_pol in cl_pol_list: @@ -1535,6 +1535,8 @@ class Geometry(object): # Optimization: Reduce lifts if connect: # log.debug("Reducing tool lifts...") + self.app.inform.emit(_("Connect: reducing tool lifts. This may take a while, please wait...")) + self.app.proc_container.update_view_text(' %s' % _("Connecting..."), clear=True) geoms = Geometry.paint_connect(geoms, polygon, tooldia, int(steps_per_circle)) return geoms @@ -1645,6 +1647,8 @@ class Geometry(object): # Optimization: Reduce lifts if connect: # log.debug("Reducing tool lifts...") + self.app.inform.emit(_("Connect: reducing tool lifts. This may take a while, please wait...")) + self.app.proc_container.update_view_text(' %s' % _("Connecting..."), clear=True) geoms_conn = Geometry.paint_connect(geom_elems, polygon_to_clear, tooldia, steps_per_circle) if geoms_conn: return geoms_conn @@ -1798,6 +1802,8 @@ class Geometry(object): # Optimization: Reduce lifts if connect: # log.debug("Reducing tool lifts...") + self.app.inform.emit(_("Connect: reducing tool lifts. This may take a while, please wait...")) + self.app.proc_container.update_view_text(' %s' % _("Connecting..."), clear=True) geoms_conn = Geometry.paint_connect(geoms, polygon, tooldia, steps_per_circle) if geoms_conn: return geoms_conn @@ -1987,6 +1993,8 @@ class Geometry(object): # Optimization: Reduce lifts if connect: # log.debug("Reducing tool lifts...") + self.app.inform.emit(_("Connect: reducing tool lifts. This may take a while, please wait...")) + self.app.proc_container.update_view_text(' %s' % _("Connecting..."), clear=True) geoms_conn = Geometry.paint_connect(geoms, polygon, tooldia, steps_per_circle) if geoms_conn: return geoms_conn @@ -1995,7 +2003,7 @@ class Geometry(object): def scale(self, xfactor, yfactor, point=None): """ - Scales all of the object's geometry by a given factor. Override + Scales all the object's geometry by a given factor. Override this method. :param xfactor: Number by which to scale on X axis. :type xfactor: float