diff --git a/CHANGELOG.md b/CHANGELOG.md index 32fc00a3..1cd82a4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ CHANGELOG for FlatCAM beta ================================================= +30.01.2022 + +- refactored the Isolation Plugin class + 29.01.2022 - upgraded/fixed the Panelize Tcl command diff --git a/appPlugins/ToolIsolation.py b/appPlugins/ToolIsolation.py index 9842eb2a..d85ca7f3 100644 --- a/appPlugins/ToolIsolation.py +++ b/appPlugins/ToolIsolation.py @@ -1657,10 +1657,20 @@ class ToolIsolation(AppTool, Gerber): :type isolated_obj: AppObjects.FlatCAMGerber.GerberObject :return: None """ - selection = self.ui.select_combo.get_value() + sel_tools = [] + table_items = self.ui.tools_table.selectedItems() + sel_rows = {t.row() for t in table_items} + for row in sel_rows: + tid = int(self.ui.tools_table.item(row, 3).text()) + sel_tools.append(tid) + if not sel_tools: + self.app.inform.emit('[ERROR_NOTCL] %s' % _("There are no tools selected in the Tool Table.")) + return 'fail' + + selection = self.ui.select_combo.get_value() if selection == 0: # ALL - self.isolate(isolated_obj=isolated_obj) + self.isolate(isolated_obj=isolated_obj, sel_tools=sel_tools, tools_storage=self.iso_tools) elif selection == 1: # Area Selection self.app.inform.emit('[WARNING_NOTCL] %s' % _("Click the start point of the area.")) @@ -1705,14 +1715,19 @@ class ToolIsolation(AppTool, Gerber): ref_obj = self.app.collection.get_by_name(self.ui.reference_combo.get_value()) ref_geo = unary_union(ref_obj.solid_geometry) use_geo = unary_union(isolated_obj.solid_geometry).difference(ref_geo) - self.isolate(isolated_obj=isolated_obj, geometry=use_geo) + self.isolate(isolated_obj=isolated_obj, geometry=use_geo, sel_tools=sel_tools, tools_storage=self.iso_tools) - def isolate(self, isolated_obj, geometry=None, limited_area=None, negative_dia=None, plot=True): + def isolate(self, isolated_obj, sel_tools, tools_storage, geometry=None, limited_area=None, negative_dia=None, + plot=True, **args): """ Creates an isolation routing geometry object in the project. :param isolated_obj: Gerber object for which to generate the isolating routing geometry :type isolated_obj: AppObjects.FlatCAMGerber.GerberObject + :param sel_tools: A list of tools to be used for isolation + :type sel_tools: list + :param tools_storage: A dictionary that holds the isolation object tools + :type tools_storage: dict :param geometry: specific geometry to isolate :type geometry: List of Shapely polygon :param limited_area: if not None isolate only this area @@ -1724,43 +1739,40 @@ class ToolIsolation(AppTool, Gerber): :return: None """ - combine = self.ui.combine_passes_cb.get_value() - tools_storage = self.iso_tools - - sorted_tools = [] - table_items = self.ui.tools_table.selectedItems() - sel_rows = {t.row() for t in table_items} - for row in sel_rows: - tid = int(self.ui.tools_table.item(row, 3).text()) - sorted_tools.append(tid) - if not sorted_tools: - self.app.inform.emit('[ERROR_NOTCL] %s' % _("There are no tools selected in the Tool Table.")) - return 'fail' + use_combine = args['combine'] if 'combine' in args else self.ui.combine_passes_cb.get_value() + use_iso_except = args['iso_except'] if 'iso_except' in args else self.ui.except_cb.get_value() + use_extra_passes = args['e_passes'] if 'e_passes' in args else self.ui.pad_passes_entry.get_value() + use_area_exception = args['area_except'] if 'area_except' in args else self.ui.except_cb.get_value() + sel_area_shape = args['area_shape'] if 'area_shape' in args else self.ui.area_shape_radio.get_value() + use_rest = args['rest'] if 'rest' in args else self.ui.rest_cb.get_value() + selection_type = args['sel_type'] if 'sel_type' in args else self.ui.select_combo.get_value() + tool_tip_shape = args['tip_shape'] if 'tip_shape' in args else self.ui.tool_shape_combo.get_value() # 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]: + for tool_iso in tools_storage: + for key in tools_storage[tool_iso]: if key == 'data': - self.iso_tools[tool_iso][key]["tools_iso_rest"] = self.ui.rest_cb.get_value() - self.iso_tools[tool_iso][key]["tools_iso_combine_passes"] = combine - self.iso_tools[tool_iso][key]["tools_iso_isoexcept"] = self.ui.except_cb.get_value() - self.iso_tools[tool_iso][key]["tools_iso_selection"] = self.ui.select_combo.get_value() - self.iso_tools[tool_iso][key]["tools_iso_area_shape"] = self.ui.area_shape_radio.get_value() - self.iso_tools[tool_iso][key]["tools_mill_job_type"] = 2 # _("Isolation") - self.iso_tools[tool_iso][key]["tools_mill_tool_shape"] = self.ui.tool_shape_combo.get_value() + tools_storage[tool_iso][key]["tools_iso_rest"] = use_rest + tools_storage[tool_iso][key]["tools_iso_combine_passes"] = use_combine + tools_storage[tool_iso][key]["tools_iso_isoexcept"] = use_iso_except + tools_storage[tool_iso][key]["tools_iso_selection"] = selection_type + tools_storage[tool_iso][key]["tools_iso_area_shape"] = sel_area_shape + tools_storage[tool_iso][key]["tools_mill_job_type"] = 2 # _("Isolation") + tools_storage[tool_iso][key]["tools_mill_tool_shape"] = tool_tip_shape - if combine: - if self.ui.rest_cb.get_value(): + if use_combine: + if use_rest: self.combined_rest(iso_obj=isolated_obj, iso2geo=geometry, tools_storage=tools_storage, lim_area=limited_area, negative_dia=negative_dia, plot=plot) else: self.combined_normal(iso_obj=isolated_obj, iso2geo=geometry, tools_storage=tools_storage, - lim_area=limited_area, negative_dia=negative_dia, plot=plot) + sel_tools=sel_tools, iso_except=use_iso_except, lim_area=limited_area, + negative_dia=negative_dia, extra_passes=use_extra_passes, plot=plot) else: prog_plot = self.app.defaults["tools_iso_plotting"] - for tool in sorted_tools: + for tool in sel_tools: tool_data = tools_storage[tool]['data'] work_geo = geometry @@ -1770,8 +1782,8 @@ class ToolIsolation(AppTool, Gerber): work_geo = isolated_obj.solid_geometry iso_t = { - 'ext': 0, - 'int': 1, + 'ext': 0, + 'int': 1, 'full': 2 }[tool_data['tools_iso_isotype']] @@ -1781,8 +1793,6 @@ class ToolIsolation(AppTool, Gerber): milling_type = tool_data['tools_iso_milling_type'] - iso_except = self.ui.except_cb.get_value() - tool_dia = tools_storage[tool]['tooldia'] for i in range(passes): iso_offset = tool_dia * ((2 * i + 1) / 2.0000001) - (i * overlap * tool_dia) @@ -1807,24 +1817,23 @@ class ToolIsolation(AppTool, Gerber): # if milling type is climb then the move is counter-clockwise around features mill_dir = 1 if milling_type == 'cl' else 0 - iso_geo = self.generate_envelope(iso_offset, mill_dir, geometry=work_geo, env_iso_type=iso_t, - nr_passes=i, prog_plot=prog_plot) + iso_geo = self.generate_envelope(isolated_obj, iso_offset, mill_dir, geometry=work_geo, + env_iso_type=iso_t, nr_passes=i, prog_plot=prog_plot) if iso_geo == 'fail': self.app.inform.emit('[ERROR_NOTCL] %s' % _("Isolation geometry could not be generated.")) continue # Extra Pads isolations pad_geo = [] - extra_passes = self.ui.pad_passes_entry.get_value() - if extra_passes > 0: + if use_extra_passes > 0: solid_geo_union = unary_union(iso_geo) extra_geo = [] - for apid in self.grb_obj.tools: - for t_geo_dict in self.grb_obj.tools[apid]['geometry']: + for apid in isolated_obj.tools: + for t_geo_dict in isolated_obj.tools[apid]['geometry']: if isinstance(t_geo_dict['follow'], Point): extra_geo.append(t_geo_dict['solid']) - for nr_pass in range(i, extra_passes + i): + for nr_pass in range(i, use_extra_passes + i): pad_pass_geo = [] for geo in extra_geo: iso_offset = tool_dia * ((2 * nr_pass + 1) / 2.0000001) - ( @@ -1849,7 +1858,7 @@ class ToolIsolation(AppTool, Gerber): # ############################################################ # ########## AREA SUBTRACTION ################################ # ############################################################ - if iso_except: + if use_iso_except: self.app.proc_container.update_view_text(' %s' % _("Subtracting Geo")) iso_geo = self.area_subtraction(iso_geo) @@ -1874,18 +1883,17 @@ class ToolIsolation(AppTool, Gerber): # ############################################################ # ########## AREA SUBTRACTION ################################ # ############################################################ - if self.ui.except_cb.get_value(): + if use_area_exception: self.app.proc_container.update_view_text(' %s' % _("Subtracting Geo")) geo_obj.solid_geometry = self.area_subtraction(geo_obj.solid_geometry) - geo_obj.tools = {'1': {}} - geo_obj.tools.update({ - '1': { + geo_obj.tools = { + 1: { 'tooldia': float(tool_dia), 'data': tool_data, 'solid_geometry': geo_obj.solid_geometry } - }) + } # detect if solid_geometry is empty and this require list flattening which is "heavy" # or just looking in the lists (they are one level depth) and if any is not empty @@ -1902,12 +1910,12 @@ class ToolIsolation(AppTool, Gerber): empty_cnt += 1 if empty_cnt == len(geo_obj.solid_geometry): - fc_obj.inform.emit('[ERROR_NOTCL] %s: %s' % ( - _("Empty Geometry in"), geo_obj.options["name"])) + msg = '[ERROR_NOTCL] %s: %s' % (_("Empty Geometry in"), geo_obj.options["name"]) + fc_obj.inform.emit(msg) return 'fail' else: - fc_obj.inform.emit('[success] %s: %s' % - (_("Isolation geometry created"), geo_obj.options["name"])) + msg = '[success] %s: %s' % (_("Isolation geometry created"), geo_obj.options["name"]) + fc_obj.inform.emit(msg) geo_obj.multigeo = True self.app.app_obj.new_object("geometry", iso_name, iso_init, plot=plot) @@ -1915,7 +1923,10 @@ class ToolIsolation(AppTool, Gerber): # clean the progressive plotted shapes if it was used if prog_plot == 'progressive': - self.temp_shapes.clear(update=True) + try: + self.temp_shapes.clear(update=True) + except Exception as err: + self.app.log.debug("ToolIsolation.isolate(). Clear temp_shapes -> %s" % str(err)) # Switch notebook to Properties page # self.app.ui.notebook.setCurrentWidget(self.app.ui.properties_tab) @@ -1940,7 +1951,7 @@ class ToolIsolation(AppTool, Gerber): :rtype: """ - log.debug("ToolIsolation.combine_rest()") + self.app.log.debug("ToolIsolation.combine_rest()") total_solid_geometry = [] @@ -2116,7 +2127,8 @@ class ToolIsolation(AppTool, Gerber): msg += coords self.app.inform_shell.emit(msg=msg) - def combined_normal(self, iso_obj, iso2geo, tools_storage, lim_area, negative_dia=None, plot=True): + def combined_normal(self, iso_obj, iso2geo, tools_storage, lim_area, sel_tools, iso_except, extra_passes=None, + negative_dia=None, plot=True, prog_plot=None): """ :param iso_obj: the isolated Gerber object @@ -2127,32 +2139,31 @@ class ToolIsolation(AppTool, Gerber): :type tools_storage: dict :param lim_area: if not None restrict isolation to this area :type lim_area: Shapely Polygon or a list of them + :param sel_tools: a list of the selected tools + :type sel_tools: list + :param iso_except: If True it will isolate except a special area + :type iso_except: bool + :param extra_passes: If not None then the value is the nr of extra isolation passes for pads only + :type extra_passes: int :param negative_dia: isolate the geometry with a negative value for the tool diameter :type negative_dia: bool :param plot: if to plot the resulting geometry object :type plot: bool + :param prog_plot: If True a progressive plot is used + :type prog_plot: bool :return: Isolated solid geometry :rtype: """ - log.debug("ToolIsolation.combined_normal()") + self.app.log.debug("ToolIsolation.combined_normal()") total_solid_geometry = [] iso_name = iso_obj.options["name"] + '_iso_combined' geometry = iso2geo - prog_plot = self.app.defaults["tools_iso_plotting"] + if prog_plot is None: + prog_plot = self.app.defaults["tools_iso_plotting"] - sorted_tools = [] - table_items = self.ui.tools_table.selectedItems() - sel_rows = {t.row() for t in table_items} - for row in sel_rows: - tid = int(self.ui.tools_table.item(row, 3).text()) - sorted_tools.append(tid) - if not sorted_tools: - self.app.inform.emit('[ERROR_NOTCL] %s' % _("There are no tools selected in the Tool Table.")) - return 'fail' - - for tool in sorted_tools: + for tool in sel_tools: tool_dia = tools_storage[tool]['tooldia'] tool_has_offset = tools_storage[tool]['data']['tools_mill_offset_type'] tool_offset_value = tools_storage[tool]['data']['tools_mill_offset_value'] @@ -2175,8 +2186,6 @@ class ToolIsolation(AppTool, Gerber): milling_type = tool_data['tools_iso_milling_type'] - iso_except = self.ui.except_cb.get_value() - outname = "%s_%.*f" % (iso_obj.options["name"], self.decimals, float(tool_dia)) internal_name = outname + "_iso" @@ -2198,7 +2207,7 @@ class ToolIsolation(AppTool, Gerber): # if milling type is climb then the move is counter-clockwise around features mill_dir = 1 if milling_type == 'cl' else 0 - iso_geo = self.generate_envelope(iso_offset, mill_dir, geometry=work_geo, env_iso_type=iso_t, + iso_geo = self.generate_envelope(iso_obj, iso_offset, mill_dir, geometry=work_geo, env_iso_type=iso_t, nr_passes=nr_pass, prog_plot=prog_plot) if iso_geo == 'fail': self.app.inform.emit('[ERROR_NOTCL] %s' % _("Isolation geometry could not be generated.")) @@ -2212,12 +2221,11 @@ class ToolIsolation(AppTool, Gerber): # Extra Pads isolations pad_geo = [] - extra_passes = self.ui.pad_passes_entry.get_value() - if extra_passes > 0: + if extra_passes is not None and extra_passes > 0: solid_geo_union = unary_union(solid_geo) extra_geo = [] - for apid in self.grb_obj.tools: - for t_geo_dict in self.grb_obj.tools[apid]['geometry']: + for apid in iso_obj.tools: + for t_geo_dict in iso_obj.tools[apid]['geometry']: if 'follow' in t_geo_dict: if isinstance(t_geo_dict['follow'], Point): extra_geo.append(t_geo_dict['solid']) @@ -2263,7 +2271,10 @@ class ToolIsolation(AppTool, Gerber): # clean the progressive plotted shapes if it was used if prog_plot == 'progressive': - self.temp_shapes.clear(update=True) + try: + self.temp_shapes.clear(update=True) + except Exception as err: + self.app.log.debug("ToolIsolation.combined_normal(). Progressive plotting -> %s" % str(err)) # remove tools without geometry for tool, tool_dict in list(tools_storage.items()): @@ -2507,16 +2518,29 @@ class ToolIsolation(AppTool, Gerber): self.app.tool_shapes.clear(update=True) + sel_tools = [] + table_items = self.ui.tools_table.selectedItems() + sel_rows = {t.row() for t in table_items} + for row in sel_rows: + tid = int(self.ui.tools_table.item(row, 3).text()) + sel_tools.append(tid) + if not sel_tools: + self.app.inform.emit('[ERROR_NOTCL] %s' % _("There are no tools selected in the Tool Table.")) + return 'fail' + if self.poly_dict: poly_list = deepcopy(list(self.poly_dict.values())) if self.ui.poly_int_cb.get_value() is True: # isolate the interior polygons with a negative tool - self.isolate(isolated_obj=self.grb_obj, geometry=poly_list, negative_dia=True) + self.isolate(isolated_obj=self.grb_obj, geometry=poly_list, negative_dia=True, sel_tools=sel_tools, + tools_storage=self.iso_tools) else: - self.isolate(isolated_obj=self.grb_obj, geometry=poly_list) + self.isolate(isolated_obj=self.grb_obj, geometry=poly_list, sel_tools=sel_tools, + tools_storage=self.iso_tools) self.poly_dict.clear() else: self.app.inform.emit('[ERROR_NOTCL] %s' % _("List of single polygons is empty. Aborting.")) + return "fail" def on_select_all_polygons(self): self.app.log.debug("ToolIsolation.on_select_all_polygons()") @@ -2738,8 +2762,19 @@ class ToolIsolation(AppTool, Gerber): if len(self.sel_rect) == 0: return + sel_tools = [] + table_items = self.ui.tools_table.selectedItems() + sel_rows = {t.row() for t in table_items} + for row in sel_rows: + tid = int(self.ui.tools_table.item(row, 3).text()) + sel_tools.append(tid) + if not sel_tools: + self.app.inform.emit('[ERROR_NOTCL] %s' % _("There are no tools selected in the Tool Table.")) + return 'fail' + self.sel_rect = unary_union(self.sel_rect) - self.isolate(isolated_obj=self.grb_obj, limited_area=self.sel_rect, plot=True) + self.isolate(isolated_obj=self.grb_obj, limited_area=self.sel_rect, sel_tools=sel_tools, + tools_storage=self.iso_tools, plot=True) self.sel_rect = [] # called on mouse move @@ -3002,7 +3037,7 @@ class ToolIsolation(AppTool, Gerber): def poly2ints(poly): return [interior for interior in poly.interiors] - def generate_envelope(self, offset, invert, geometry=None, env_iso_type=2, nr_passes=0, + def generate_envelope(self, iso_obj, offset, invert, geometry=None, env_iso_type=2, nr_passes=0, prog_plot=False): """ Isolation_geometry produces an envelope that is going on the left of the geometry @@ -3012,6 +3047,7 @@ class ToolIsolation(AppTool, Gerber): the other passes overlap preceding ones and cut the left over copper. It is better for them to cut on the right side of the left over copper i.e on the left side of the features. + :param iso_obj: The Gerber object to be isolated :param offset: Offset distance to be passed to the obj.isolation_geometry() method :type offset: float :param invert: If to invert the direction of geometry (CW to CCW or reverse) @@ -3029,10 +3065,10 @@ class ToolIsolation(AppTool, Gerber): """ try: - geom_shp = self.grb_obj.isolation_geometry(offset, geometry=geometry, iso_type=env_iso_type, - passes=nr_passes, prog_plot=prog_plot) + geom_shp = iso_obj.isolation_geometry(offset, geometry=geometry, iso_type=env_iso_type, + passes=nr_passes, prog_plot=prog_plot) except Exception as e: - log.error('ToolIsolation.generate_envelope() --> %s' % str(e)) + self.app.log.error('ToolIsolation.generate_envelope() --> %s' % str(e)) return 'fail' if isinstance(geom_shp, (MultiPolygon, MultiLineString)): @@ -3056,10 +3092,10 @@ class ToolIsolation(AppTool, Gerber): elif isinstance(geom, LinearRing) and geom is not None: geom = Polygon(geom.coords[::-1]) else: - log.debug("ToolIsolation.generate_envelope() Error --> Unexpected Geometry %s" % - type(geom)) + msg = "ToolIsolation.generate_envelope() Error --> Unexpected Geometry %s" % type(geom) + self.app.log.debug(msg) except Exception as e: - log.error("ToolIsolation.generate_envelope() Error --> %s" % str(e)) + self.app.log.error("ToolIsolation.generate_envelope() Error --> %s" % str(e)) return 'fail' return geom diff --git a/tclCommands/TclCommandIsolate.py b/tclCommands/TclCommandIsolate.py index b0117af1..918f02ed 100644 --- a/tclCommands/TclCommandIsolate.py +++ b/tclCommands/TclCommandIsolate.py @@ -82,16 +82,18 @@ class TclCommandIsolate(TclCommandSignaled): par = args['combine'].capitalize() except AttributeError: par = args['combine'] - args['combine'] = bool(eval(par)) + args['combine'] = bool(eval(str(par))) else: args['combine'] = bool(eval(str(self.app.defaults["tools_iso_combine_passes"]))) obj = self.app.collection.get_by_name(name) if obj is None: self.raise_tcl_error("Object not found: %s" % name) + return "fail" if obj.kind != 'gerber': self.raise_tcl_error('Expected GerberObject, got %s %s.' % (name, type(obj))) + return "fail" args.pop('name', None) obj.isolate(plot=False, **args)