From 15d94404a7c059c8b200d0ef31bfa303c1d8599b Mon Sep 17 00:00:00 2001 From: Marius Stanciu Date: Sat, 30 May 2020 03:06:47 +0300 Subject: [PATCH] - made confirmation messages for the values that are modified not to be printed in the Shell - Isolation Tool: working on the Rest machining: almost there, perhaps I will use multiprocessing --- AppGUI/ObjectUI.py | 15 +- AppTool.py | 14 +- AppTools/ToolIsolation.py | 327 +++++++++++++++++++++++++------------- App_Main.py | 12 +- CHANGELOG.md | 5 + 5 files changed, 249 insertions(+), 124 deletions(-) diff --git a/AppGUI/ObjectUI.py b/AppGUI/ObjectUI.py index acb0a55d..085fe23c 100644 --- a/AppGUI/ObjectUI.py +++ b/AppGUI/ObjectUI.py @@ -153,17 +153,20 @@ class ObjectUI(QtWidgets.QWidget): def confirmation_message(self, accepted, minval, maxval): if accepted is False: - self.app.inform.emit('[WARNING_NOTCL] %s: [%.*f, %.*f]' % - (_("Edited value is out of range"), self.decimals, minval, self.decimals, maxval)) + self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%.*f, %.*f]' % (_("Edited value is out of range"), + self.decimals, + minval, + self.decimals, + maxval), False) else: - self.app.inform.emit('[success] %s' % _("Edited value is within limits.")) + self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False) def confirmation_message_int(self, accepted, minval, maxval): if accepted is False: - self.app.inform.emit('[WARNING_NOTCL] %s: [%d, %d]' % - (_("Edited value is out of range"), minval, maxval)) + self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%d, %d]' % + (_("Edited value is out of range"), minval, maxval), False) else: - self.app.inform.emit('[success] %s' % _("Edited value is within limits.")) + self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False) class GerberObjectUI(ObjectUI): diff --git a/AppTool.py b/AppTool.py index 8391d76d..f288cdf0 100644 --- a/AppTool.py +++ b/AppTool.py @@ -277,16 +277,20 @@ class AppTool(QtWidgets.QWidget): def confirmation_message(self, accepted, minval, maxval): if accepted is False: - self.app.inform.emit('[WARNING_NOTCL] %s: [%.*f, %.*f]' % - (_("Edited value is out of range"), self.decimals, minval, self.decimals, maxval)) + self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%.*f, %.*f]' % (_("Edited value is out of range"), + self.decimals, + minval, + self.decimals, + maxval), False) else: - self.app.inform.emit('[success] %s' % _("Edited value is within limits.")) + self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False) def confirmation_message_int(self, accepted, minval, maxval): if accepted is False: - self.app.inform.emit('[WARNING_NOTCL] %s: [%d, %d]' % (_("Edited value is out of range"), minval, maxval)) + self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%d, %d]' % + (_("Edited value is out of range"), minval, maxval), False) else: - self.app.inform.emit('[success] %s' % _("Edited value is within limits.")) + self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False) def sizeHint(self): """ diff --git a/AppTools/ToolIsolation.py b/AppTools/ToolIsolation.py index fee2a6ee..570c7cf0 100644 --- a/AppTools/ToolIsolation.py +++ b/AppTools/ToolIsolation.py @@ -136,7 +136,7 @@ class ToolIsolation(AppTool, Gerber): _("This is the Tool Number.\n" "Non copper clearing will start with the tool with the biggest \n" "diameter, continuing until there are no more tools.\n" - "Only tools that create NCC clearing geometry will still be present\n" + "Only tools that create Isolation geometry will still be present\n" "in the resulting geometry. This is because with some tools\n" "this function will not be able to create painting geometry.") ) @@ -163,19 +163,19 @@ class ToolIsolation(AppTool, Gerber): self.tools_box.addLayout(grid1) # Tool order - self.ncc_order_label = QtWidgets.QLabel('%s:' % _('Tool order')) - self.ncc_order_label.setToolTip(_("This set the way that the tools in the tools table are used.\n" - "'No' --> means that the used order is the one in the tool table\n" - "'Forward' --> means that the tools will be ordered from small to big\n" - "'Reverse' --> means that the tools will ordered from big to small\n\n" - "WARNING: using rest machining will automatically set the order\n" - "in reverse and disable this control.")) + self.order_label = QtWidgets.QLabel('%s:' % _('Tool order')) + self.order_label.setToolTip(_("This set the way that the tools in the tools table are used.\n" + "'No' --> means that the used order is the one in the tool table\n" + "'Forward' --> means that the tools will be ordered from small to big\n" + "'Reverse' --> means that the tools will ordered from big to small\n\n" + "WARNING: using rest machining will automatically set the order\n" + "in reverse and disable this control.")) self.order_radio = RadioSet([{'label': _('No'), 'value': 'no'}, {'label': _('Forward'), 'value': 'fwd'}, {'label': _('Reverse'), 'value': 'rev'}]) - grid1.addWidget(self.ncc_order_label, 1, 0) + grid1.addWidget(self.order_label, 1, 0) grid1.addWidget(self.order_radio, 1, 1) separator_line = QtWidgets.QFrame() @@ -1082,10 +1082,7 @@ class ToolIsolation(AppTool, Gerber): sorted_tools = [] for k, v in self.iso_tools.items(): - if self.units == "IN": - sorted_tools.append(float('%.*f' % (self.decimals, float(v['tooldia'])))) - else: - sorted_tools.append(float('%.*f' % (self.decimals, float(v['tooldia'])))) + sorted_tools.append(float('%.*f' % (self.decimals, float(v['tooldia'])))) order = self.order_radio.get_value() if order == 'fwd': @@ -1323,14 +1320,14 @@ class ToolIsolation(AppTool, Gerber): def on_rest_machining_check(self, state): if state: self.order_radio.set_value('rev') - self.ncc_order_label.setDisabled(True) + self.order_label.setDisabled(True) self.order_radio.setDisabled(True) self.old_combine_state = self.combine_passes_cb.get_value() self.combine_passes_cb.set_value(True) self.combine_passes_cb.setDisabled(True) else: - self.ncc_order_label.setDisabled(False) + self.order_label.setDisabled(False) self.order_radio.setDisabled(False) self.combine_passes_cb.set_value(self.old_combine_state) @@ -1883,101 +1880,102 @@ class ToolIsolation(AppTool, Gerber): total_solid_geometry = [] iso_name = iso_obj.options["name"] - geometry = iso2geo + work_geo = iso_obj.solid_geometry if iso2geo is None else iso2geo - for tool in tools_storage: - tool_dia = tools_storage[tool]['tooldia'] - tool_type = tools_storage[tool]['tool_type'] - tool_data = tools_storage[tool]['data'] + sorted_tools = [] + for k, v in self.iso_tools.items(): + sorted_tools.append(float('%.*f' % (self.decimals, float(v['tooldia'])))) - to_follow = tool_data['tools_iso_follow'] + order = self.order_radio.get_value() + if order == 'fwd': + sorted_tools.sort(reverse=False) + elif order == 'rev': + sorted_tools.sort(reverse=True) + else: + pass - work_geo = geometry - if work_geo is None: - work_geo = iso_obj.follow_geometry if to_follow else iso_obj.solid_geometry + for sorted_tool in sorted_tools: + for tool in tools_storage: + if float('%.*f' % (self.decimals, tools_storage[tool]['tooldia'])) == sorted_tool: - iso_t = { - 'ext': 0, - 'int': 1, - 'full': 2 - }[tool_data['tools_iso_isotype']] + tool_dia = tools_storage[tool]['tooldia'] + tool_type = tools_storage[tool]['tool_type'] + tool_data = tools_storage[tool]['data'] - passes = tool_data['tools_iso_passes'] - overlap = tool_data['tools_iso_overlap'] - overlap /= 100.0 + iso_t = { + 'ext': 0, + 'int': 1, + 'full': 2 + }[tool_data['tools_iso_isotype']] - milling_type = tool_data['tools_iso_milling_type'] + passes = tool_data['tools_iso_passes'] + overlap = tool_data['tools_iso_overlap'] + overlap /= 100.0 - iso_except = tool_data['tools_iso_isoexcept'] + milling_type = tool_data['tools_iso_milling_type'] + # if milling type is climb then the move is counter-clockwise around features + mill_dir = True if milling_type == 'cl' else False - outname = "%s_%.*f" % (iso_obj.options["name"], self.decimals, float(tool_dia)) + iso_except = tool_data['tools_iso_isoexcept'] - iso_name = outname + "_iso" - if iso_t == 0: - iso_name = outname + "_ext_iso" - elif iso_t == 1: - iso_name = outname + "_int_iso" + outname = "%s_%.*f" % (iso_obj.options["name"], self.decimals, float(tool_dia)) - # transfer the Cut Z and Vtip and VAngle values in case that we use the V-Shape tool in Gerber UI - if tool_type.lower() == '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' - tool_data.update({ - "name": iso_name, - "cutz": new_cutz, - "vtipdia": new_vtipdia, - "vtipangle": new_vtipangle, - }) - else: - tool_data.update({ - "name": iso_name, - }) - tool_type = 'C1' + iso_name = outname + "_iso" + if iso_t == 0: + iso_name = outname + "_ext_iso" + elif iso_t == 1: + iso_name = outname + "_int_iso" - solid_geo = [] - for nr_pass in range(passes): - iso_offset = tool_dia * ((2 * nr_pass + 1) / 2.0000001) - (nr_pass * overlap * tool_dia) + # transfer the Cut Z and Vtip and VAngle values in case that we use the V-Shape tool in Gerber UI + if tool_type.lower() == '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' + tool_data.update({ + "name": iso_name, + "cutz": new_cutz, + "vtipdia": new_vtipdia, + "vtipangle": new_vtipangle, + }) + else: + tool_data.update({ + "name": iso_name, + }) + tool_type = 'C1' - # if milling type is climb then the move is counter-clockwise around features - mill_dir = 1 if milling_type == 'cl' else 0 + solid_geo, work_geo = self.generate_rest_geometry(geometry=work_geo, tooldia=tool_dia, + passes=passes, overlap=overlap, invert=mill_dir, + env_iso_type=iso_t) - iso_geo = self.generate_envelope(iso_offset, mill_dir, geometry=work_geo, env_iso_type=iso_t, - follow=to_follow, nr_passes=nr_pass) - if iso_geo == 'fail': - self.app.inform.emit('[ERROR_NOTCL] %s' % _("Isolation geometry could not be generated.")) - continue - try: - for geo in iso_geo: - solid_geo.append(geo) - except TypeError: - solid_geo.append(iso_geo) + # ############################################################ + # ########## AREA SUBTRACTION ################################ + # ############################################################ + if iso_except: + self.app.proc_container.update_view_text(' %s' % _("Subtracting Geo")) + solid_geo = self.area_subtraction(solid_geo) - # ############################################################ - # ########## AREA SUBTRACTION ################################ - # ############################################################ - if iso_except: - self.app.proc_container.update_view_text(' %s' % _("Subtracting Geo")) - solid_geo = self.area_subtraction(solid_geo) + if lim_area: + self.app.proc_container.update_view_text(' %s' % _("Intersecting Geo")) + solid_geo = self.area_intersection(solid_geo, intersection_geo=lim_area) - if lim_area: - self.app.proc_container.update_view_text(' %s' % _("Intersecting Geo")) - solid_geo = self.area_intersection(solid_geo, intersection_geo=lim_area) + tools_storage.update({ + tool: { + 'tooldia': float(tool_dia), + 'offset': 'Path', + 'offset_value': 0.0, + 'type': _('Rough'), + 'tool_type': tool_type, + 'data': tool_data, + 'solid_geometry': deepcopy(solid_geo) + } + }) - tools_storage.update({ - tool: { - 'tooldia': float(tool_dia), - 'offset': 'Path', - 'offset_value': 0.0, - 'type': _('Rough'), - 'tool_type': tool_type, - 'data': tool_data, - 'solid_geometry': deepcopy(solid_geo) - } - }) + total_solid_geometry += solid_geo - total_solid_geometry += solid_geo + # if the geometry is all isolated + if not work_geo: + break def iso_init(geo_obj, app_obj): geo_obj.options["cnctooldia"] = str(tool_dia) @@ -1989,11 +1987,12 @@ class ToolIsolation(AppTool, Gerber): if len(tools_storage) > 1: geo_obj.multigeo = True else: - if to_follow: - passes_no = 1 - else: + try: passes_no = float(tools_storage[0]['data']['tools_iso_passes']) - geo_obj.multigeo = True if passes_no > 1 else False + geo_obj.multigeo = True if passes_no > 1 else False + except KeyError as e: + log.debug("ToolIsolation.combined_rest.iso_init() --> KeyError: %s" % str(e)) + geo_obj.multogeo = False # 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 @@ -2018,6 +2017,20 @@ class ToolIsolation(AppTool, Gerber): self.app.app_obj.new_object("geometry", iso_name, iso_init, plot=plot) + # the tools are finished but the isolation is not finished therefore it failed + if work_geo: + self.app.inform.emit("[WARNING] %s" % _("Partial failure. The geometry was processed with all tools.\n" + "But there are still un-isolated geometry elements. " + "Try to include a tool with smaller diameter.")) + self.app.shell_message(msg=_("The following are coordinates for the copper features " + "that could not be isolated:")) + msg = '' + for geo in work_geo: + pt = geo.representative_point() + coords = '(%s, %s), ' % (str(pt.x), str(pt.y)) + msg += coords + self.app.shell_message(msg=msg) + def combined_normal(self, iso_obj, iso2geo, tools_storage, lim_area, plot=True): """ @@ -2112,16 +2125,16 @@ class ToolIsolation(AppTool, Gerber): except TypeError: solid_geo.append(iso_geo) - # ############################################################ - # ########## AREA SUBTRACTION ################################ - # ############################################################ - if iso_except: - self.app.proc_container.update_view_text(' %s' % _("Subtracting Geo")) - solid_geo = self.area_subtraction(solid_geo) + # ############################################################ + # ########## AREA SUBTRACTION ################################ + # ############################################################ + if iso_except: + self.app.proc_container.update_view_text(' %s' % _("Subtracting Geo")) + solid_geo = self.area_subtraction(solid_geo) - if lim_area: - self.app.proc_container.update_view_text(' %s' % _("Intersecting Geo")) - solid_geo = self.area_intersection(solid_geo, intersection_geo=lim_area) + if lim_area: + self.app.proc_container.update_view_text(' %s' % _("Intersecting Geo")) + solid_geo = self.area_intersection(solid_geo, intersection_geo=lim_area) tools_storage.update({ tool: { @@ -2675,6 +2688,14 @@ class ToolIsolation(AppTool, Gerber): def poly2rings(poly): return [poly.exterior] + [interior for interior in poly.interiors] + @staticmethod + def poly2ext(poly): + return [poly.exterior] + + @staticmethod + def poly2ints(poly): + return [interior for interior in poly.interiors] + def generate_envelope(self, 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 @@ -2734,6 +2755,98 @@ class ToolIsolation(AppTool, Gerber): return 'fail' return geom + @staticmethod + def generate_rest_geometry(geometry, tooldia, passes, overlap, invert, env_iso_type=2): + """ + Will try to isolate the geometry and return a tuple made of list of paths made through isolation + and a list of Shapely Polygons that could not be isolated + + :param geometry: A list of Shapely Polygons to be isolated + :type geometry: list + :param tooldia: The tool diameter used to do the isolation + :type tooldia: float + :param passes: Number of passes that will made the isolation + :type passes: int + :param overlap: How much to overlap the previous pass; in percentage [0.00, 99.99]% + :type overlap: float + :param invert: If to invert the direction of the resulting isolated geometries + :type invert: bool + :param env_iso_type: can be either 0 = keep exteriors or 1 = keep interiors or 2 = keep all paths + :type env_iso_type: int + :return: Tuple made from list of isolating paths and list of not isolated Polygons + :rtype: tuple + """ + + isolated_geo = [] + not_isolated_geo = [] + + work_geo = [] + + for idx, geo in enumerate(geometry): + good_pass_iso = [] + start_idx = idx + 1 + + for nr_pass in range(passes): + iso_offset = tooldia * ((2 * nr_pass + 1) / 2.0) - (nr_pass * overlap * tooldia) + buf_chek = iso_offset * 2 + check_geo = geo.buffer(buf_chek) + + intersect_flag = False + # find if current pass for current geo is valid (no intersection with other geos)) + for geo_search_idx in range(idx): + if check_geo.intersects(geometry[geo_search_idx]): + intersect_flag = True + break + + if intersect_flag == False: + for geo_search_idx in range(start_idx, len(geometry)): + if check_geo.intersects(geometry[geo_search_idx]): + intersect_flag = True + break + + # if we had an intersection do nothing, else add the geo to the good pass isolations + if intersect_flag is False: + good_pass_iso.append(geo.buffer(iso_offset)) + + if good_pass_iso: + work_geo += good_pass_iso + else: + not_isolated_geo.append(geo) + + if invert: + try: + pl = [] + for p in work_geo: + 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])) + work_geo = MultiPolygon(pl) + except TypeError: + if isinstance(work_geo, Polygon) and work_geo is not None: + work_geo = [Polygon(work_geo.exterior.coords[::-1], work_geo.interiors)] + elif isinstance(work_geo, LinearRing) and work_geo is not None: + work_geo = [Polygon(work_geo.coords[::-1])] + else: + log.debug("ToolIsolation.generate_rest_geometry() Error --> Unexpected Geometry %s" % + type(work_geo)) + except Exception as e: + log.debug("ToolIsolation.generate_rest_geometry() Error --> %s" % str(e)) + return 'fail', 'fail' + + if env_iso_type == 0: # exterior + for geo in work_geo: + isolated_geo.append(geo.exterior) + elif env_iso_type == 1: # interiors + for geo in work_geo: + isolated_geo += [interior for interior in geo.interiors] + else: # exterior + interiors + for geo in work_geo: + isolated_geo += [geo.exterior] + [interior for interior in geo.interiors] + + return isolated_geo, not_isolated_geo + def on_iso_tool_add_from_db_executed(self, tool): """ Here add the tool from DB in the selected geometry object @@ -2809,7 +2922,7 @@ class ToolIsolation(AppTool, Gerber): 'solid_geometry': [] } }) - self.iso_tools[tooluid]['data']['name'] = '_ncc' + self.iso_tools[tooluid]['data']['name'] = '_iso' self.app.inform.emit('[success] %s' % _("New tool added to Tool Table.")) diff --git a/App_Main.py b/App_Main.py index 5efc78a4..90a65f1c 100644 --- a/App_Main.py +++ b/App_Main.py @@ -9845,12 +9845,12 @@ class App(QtCore.QObject): """ Shows a message on the FlatCAM Shell - :param msg: Message to display. - :param show: Opens the shell. - :param error: Shows the message as an error. - :param warning: Shows the message as an warning. - :param success: Shows the message as an success. - :param selected: Indicate that something was selected on canvas + :param msg: Message to display. + :param show: Opens the shell. + :param error: Shows the message as an error. + :param warning: Shows the message as an warning. + :param success: Shows the message as an success. + :param selected: Indicate that something was selected on canvas :return: None """ if show: diff --git a/CHANGELOG.md b/CHANGELOG.md index 676f308d..1d61aaf1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ CHANGELOG for FlatCAM beta ================================================= +30.05.2020 + +- made confirmation messages for the values that are modified not to be printed in the Shell +- Isolation Tool: working on the Rest machining: almost there, perhaps I will use multiprocessing + 29.05.2020 - fixed the Tool Isolation when using the 'follow' parameter