diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 9a4ff45c..16912b03 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -105,7 +105,7 @@ class App(QtCore.QObject): # Version and VERSION DATE ########### # #################################### version = 8.97 - version_date = "2019/08/31" + version_date = "2019/09/07" beta = True # current date now @@ -1788,6 +1788,9 @@ class App(QtCore.QObject): self.ui.fa_defaults_form.fa_gerber_group.grb_list_btn.clicked.connect( lambda: self.on_register_files(obj_type='gerber')) + # connect the abort_all_tasks related slots to the related signals + self.proc_container.idle_flag.connect(self.app_is_idle) + self.log.debug("Finished connecting Signals.") # this is a flag to signal to other tools that the ui tooltab is locked and not accessible @@ -2036,7 +2039,7 @@ class App(QtCore.QObject): self.shell.setWindowTitle("FlatCAM Shell") self.shell.resize(*self.defaults["global_shell_shape"]) self.shell.append_output("FlatCAM %s - " % self.version) - self.shell.append_output(_("Type help to get started\n\n")) + self.shell.append_output(_("Open Source Software - Type help to get started\n\n")) self.init_tcl() @@ -2193,6 +2196,9 @@ class App(QtCore.QObject): self.isHovering = False self.notHovering = True + # when True, the app has to return from any thread + self.abort_flag = False + # ######################################################### # ### Save defaults to factory_defaults.FlatConfig file ### # ### It's done only once after install ################### @@ -3544,9 +3550,12 @@ class App(QtCore.QObject): "2D Computer-Aided Printed Circuit Board
" "Manufacturing.
" "
" - "(c) 2014-2019 Juan Pablo Caram
" + " License:
" + "Licensed under MIT license (c)2014 - 2019" "
" - " Main Contributors:
" + "
" + " Programmers:
" + " Juan Pablo Caram
" "Denis Hayrullin
" "Kamil Sopko
" "Marius Stanciu
" @@ -5693,6 +5702,17 @@ class App(QtCore.QObject): except Exception as e: return "Operation failed: %s" % str(e) + def abort_all_tasks(self): + if self.abort_flag is False: + self.inform.emit(_("Aborting. The current task will be gracefully closed as soon as possible...")) + self.abort_flag = True + + def app_is_idle(self): + if self.abort_flag: + self.inform.emit('[WARNING_NOTCL] %s' % + _("The current task was gracefully closed on user request...")) + self.abort_flag = False + def on_set_zero_click(self, event): # this function will be available only for mouse left click pos = [] @@ -9686,4 +9706,13 @@ class ArgsThread(QtCore.QObject): def run(self): self.my_loop(self.address) + +class GracefulException(Exception): + # Graceful Exception raised when the user is requesting to cancel the current threaded task + def __init__(self): + super().__init__() + + def __str__(self): + return ('\n\n%s' % _("The user requested a graceful exit of the current task.")) + # end of file diff --git a/FlatCAMProcess.py b/FlatCAMProcess.py index f28b5a66..65e1d7c7 100644 --- a/FlatCAMProcess.py +++ b/FlatCAMProcess.py @@ -128,6 +128,8 @@ class FCProcessContainer(object): class FCVisibleProcessContainer(QtCore.QObject, FCProcessContainer): something_changed = QtCore.pyqtSignal() + # this will signal that the application is IDLE + idle_flag = QtCore.pyqtSignal() def __init__(self, view): assert isinstance(view, FlatCAMActivityView), \ @@ -161,6 +163,7 @@ class FCVisibleProcessContainer(QtCore.QObject, FCProcessContainer): def update_view(self): if len(self.procs) == 0: self.view.set_idle() + self.idle_flag.emit() self.new_text = '' elif len(self.procs) == 1: diff --git a/README.md b/README.md index a11029b2..2d0c5713 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,11 @@ CAD program, and create G-Code for Isolation routing. ================================================= +7.09.2019 + +- added a method to gracefully exit from threaded tasks and implemented it for the NCC Tool and for the Paint Tool +- modified the on_about() function to reflect the reality in 2019 - FlatCAM it is an Open Source contributed software + 6.09.2019 - remade visibility threaded diff --git a/camlib.py b/camlib.py index 1a494444..f247972a 100644 --- a/camlib.py +++ b/camlib.py @@ -797,8 +797,8 @@ class Geometry(object): boundary = self.solid_geometry.envelope return boundary.difference(self.solid_geometry) - @staticmethod - def clear_polygon(polygon, tooldia, steps_per_circle, overlap=0.15, connect=True, contour=True): + + def clear_polygon(self, polygon, tooldia, steps_per_circle, overlap=0.15, connect=True, contour=True): """ Creates geometry inside a polygon for a tool to cover the whole area. @@ -852,6 +852,9 @@ class Geometry(object): geoms.insert(i) while True: + if self.app.abort_flag: + # graceful abort requested by the user + raise FlatCAMApp.GracefulException # Can only result in a Polygon or MultiPolygon current = current.buffer(-tooldia * (1 - overlap), int(int(steps_per_circle) / 4)) @@ -880,8 +883,7 @@ class Geometry(object): return geoms - @staticmethod - def clear_polygon2(polygon_to_clear, tooldia, steps_per_circle, seedpoint=None, overlap=0.15, + def clear_polygon2(self, polygon_to_clear, tooldia, steps_per_circle, seedpoint=None, overlap=0.15, connect=True, contour=True): """ Creates geometry inside a polygon for a tool to cover @@ -928,7 +930,11 @@ class Geometry(object): # Grow from seed until outside the box. The polygons will # never have an interior, so take the exterior LinearRing. - while 1: + while True: + if self.app.abort_flag: + # graceful abort requested by the user + raise FlatCAMApp.GracefulException + path = Point(seedpoint).buffer(radius, int(steps_per_circle / 4)).exterior path = path.intersection(path_margin) @@ -971,8 +977,7 @@ class Geometry(object): return geoms - @staticmethod - def clear_polygon3(polygon, tooldia, steps_per_circle, overlap=0.15, connect=True, contour=True): + def clear_polygon3(self, polygon, tooldia, steps_per_circle, overlap=0.15, connect=True, contour=True): """ Creates geometry inside a polygon for a tool to cover the whole area. @@ -1007,6 +1012,10 @@ class Geometry(object): # First line y = top - tooldia / 1.99999999 while y > bot + tooldia / 1.999999999: + if self.app.abort_flag: + # graceful abort requested by the user + raise FlatCAMApp.GracefulException + line = LineString([(left, y), (right, y)]) lines.append(line) y -= tooldia * (1 - overlap) diff --git a/flatcamGUI/FlatCAMGUI.py b/flatcamGUI/FlatCAMGUI.py index ea26d14e..8974525e 100644 --- a/flatcamGUI/FlatCAMGUI.py +++ b/flatcamGUI/FlatCAMGUI.py @@ -1253,7 +1253,15 @@ class FlatCAMGUI(QtWidgets.QMainWindow): ALT+F10  Toggle Full Screen + + +   +   + + CTRL+ALT+X +  Abort current task (gracefully) +     @@ -2163,7 +2171,12 @@ class FlatCAMGUI(QtWidgets.QMainWindow): key = event.key if self.app.call_source == 'app': - if modifiers == QtCore.Qt.ControlModifier: + if modifiers == QtCore.Qt.ControlModifier | QtCore.Qt.AltModifier: + if key == QtCore.Qt.Key_X: + self.app.abort_all_tasks() + return + + elif modifiers == QtCore.Qt.ControlModifier: if key == QtCore.Qt.Key_A: self.app.on_selectall() diff --git a/flatcamGUI/GUIElements.py b/flatcamGUI/GUIElements.py index 56d3e50f..a1b85f98 100644 --- a/flatcamGUI/GUIElements.py +++ b/flatcamGUI/GUIElements.py @@ -1743,7 +1743,7 @@ class _BrowserTextEdit(QTextEdit): def clear(self): QTextEdit.clear(self) - text = "FlatCAM %s - Type help to get started\n\n" % self.version + text = "FlatCAM %s - Open Source Software - Type help to get started\n\n" % self.version text = html.escape(text) text = text.replace('\n', '
') self.moveCursor(QTextCursor.End) diff --git a/flatcamTools/ToolNonCopperClear.py b/flatcamTools/ToolNonCopperClear.py index 2f83dc91..8f64fee7 100644 --- a/flatcamTools/ToolNonCopperClear.py +++ b/flatcamTools/ToolNonCopperClear.py @@ -1429,6 +1429,9 @@ class NonCopperClear(FlatCAMTool, Gerber): geo_buff_list = [] for poly in geo_n: + if self.app.abort_flag: + # graceful abort requested by the user + raise FlatCAMApp.GracefulException geo_buff_list.append(poly.buffer(distance=ncc_margin, join_style=base.JOIN_STYLE.mitre)) bounding_box = cascaded_union(geo_buff_list) @@ -1444,6 +1447,9 @@ class NonCopperClear(FlatCAMTool, Gerber): geo_buff_list = [] for poly in geo_n: + if self.app.abort_flag: + # graceful abort requested by the user + raise FlatCAMApp.GracefulException geo_buff_list.append(poly.buffer(distance=ncc_margin, join_style=base.JOIN_STYLE.mitre)) bounding_box = cascaded_union(geo_buff_list) @@ -1537,6 +1543,10 @@ class NonCopperClear(FlatCAMTool, Gerber): else: try: for geo_elem in isolated_geo: + if self.app.abort_flag: + # graceful abort requested by the user + raise FlatCAMApp.GracefulException + if isinstance(geo_elem, Polygon): for ring in self.poly2rings(geo_elem): new_geo = ring.intersection(bounding_box) @@ -1627,6 +1637,10 @@ class NonCopperClear(FlatCAMTool, Gerber): cp = None for tool in sorted_tools: log.debug("Starting geometry processing for tool: %s" % str(tool)) + if self.app.abort_flag: + # graceful abort requested by the user + raise FlatCAMApp.GracefulException + app_obj.inform.emit( '[success] %s %s%s %s' % (_('NCC Tool clearing with tool diameter = '), str(tool), @@ -1661,6 +1675,9 @@ class NonCopperClear(FlatCAMTool, Gerber): if len(area.geoms) > 0: pol_nr = 0 for p in area.geoms: + if self.app.abort_flag: + # graceful abort requested by the user + raise FlatCAMApp.GracefulException if p is not None: try: if isinstance(p, Polygon): @@ -1836,6 +1853,10 @@ class NonCopperClear(FlatCAMTool, Gerber): else: try: for geo_elem in isolated_geo: + if self.app.abort_flag: + # graceful abort requested by the user + raise FlatCAMApp.GracefulException + if isinstance(geo_elem, Polygon): for ring in self.poly2rings(geo_elem): new_geo = ring.intersection(bounding_box) @@ -1858,21 +1879,24 @@ class NonCopperClear(FlatCAMTool, Gerber): if new_geo and not new_geo.is_empty: new_geometry.append(new_geo) except TypeError: - if isinstance(isolated_geo, Polygon): - for ring in self.poly2rings(isolated_geo): - new_geo = ring.intersection(bounding_box) - if new_geo: - if not new_geo.is_empty: - new_geometry.append(new_geo) - elif isinstance(isolated_geo, LineString): - new_geo = isolated_geo.intersection(bounding_box) - if new_geo and not new_geo.is_empty: - new_geometry.append(new_geo) - elif isinstance(isolated_geo, MultiLineString): - for line_elem in isolated_geo: - new_geo = line_elem.intersection(bounding_box) + try: + if isinstance(isolated_geo, Polygon): + for ring in self.poly2rings(isolated_geo): + new_geo = ring.intersection(bounding_box) + if new_geo: + if not new_geo.is_empty: + new_geometry.append(new_geo) + elif isinstance(isolated_geo, LineString): + new_geo = isolated_geo.intersection(bounding_box) if new_geo and not new_geo.is_empty: new_geometry.append(new_geo) + elif isinstance(isolated_geo, MultiLineString): + for line_elem in isolated_geo: + new_geo = line_elem.intersection(bounding_box) + if new_geo and not new_geo.is_empty: + new_geometry.append(new_geo) + except Exception as e: + pass # a MultiLineString geometry element will show that the isolation is broken for this tool for geo_e in new_geometry: @@ -1919,6 +1943,10 @@ class NonCopperClear(FlatCAMTool, Gerber): _("Could not get the extent of the area to be non copper cleared.")) return 'fail' + if self.app.abort_flag: + # graceful abort requested by the user + raise FlatCAMApp.GracefulException + if type(empty) is Polygon: empty = MultiPolygon([empty]) @@ -1929,6 +1957,10 @@ class NonCopperClear(FlatCAMTool, Gerber): # Generate area for each tool while sorted_tools: + if self.app.abort_flag: + # graceful abort requested by the user + raise FlatCAMApp.GracefulException + tool = sorted_tools.pop(0) log.debug("Starting geometry processing for tool: %s" % str(tool)) @@ -1945,6 +1977,9 @@ class NonCopperClear(FlatCAMTool, Gerber): # Area to clear for poly in cleared_by_last_tool: + if self.app.abort_flag: + # graceful abort requested by the user + raise FlatCAMApp.GracefulException try: area = area.difference(poly) except Exception as e: @@ -1973,6 +2008,10 @@ class NonCopperClear(FlatCAMTool, Gerber): if len(area.geoms) > 0: pol_nr = 0 for p in area.geoms: + if self.app.abort_flag: + # graceful abort requested by the user + raise FlatCAMApp.GracefulException + if p is not None: if isinstance(p, Polygon): try: @@ -2029,6 +2068,10 @@ class NonCopperClear(FlatCAMTool, Gerber): old_disp_number = disp_number # log.debug("Polygons cleared: %d. Percentage done: %d%%" % (pol_nr, disp_number)) + if self.app.abort_flag: + # graceful abort requested by the user + raise FlatCAMApp.GracefulException + # check if there is a geometry at all in the cleared geometry if cleared_geo: # Overall cleared area @@ -2039,10 +2082,14 @@ class NonCopperClear(FlatCAMTool, Gerber): # here we store the poly's already processed in the original geometry by the current tool # into cleared_by_last_tool list - # this will be sustracted from the original geometry_to_be_cleared and make data for + # this will be sutracted from the original geometry_to_be_cleared and make data for # the next tool buffer_value = tool_used / 2 for p in cleared_area: + if self.app.abort_flag: + # graceful abort requested by the user + raise FlatCAMApp.GracefulException + poly = p.buffer(buffer_value) cleared_by_last_tool.append(poly) @@ -2091,6 +2138,9 @@ class NonCopperClear(FlatCAMTool, Gerber): app_obj.new_object("geometry", name, gen_clear_area_rest) else: app_obj.new_object("geometry", name, gen_clear_area) + except FlatCAMApp.GracefulException: + proc.done() + return except Exception as e: proc.done() traceback.print_stack() diff --git a/flatcamTools/ToolPaint.py b/flatcamTools/ToolPaint.py index ba35b973..ca45fd3c 100644 --- a/flatcamTools/ToolPaint.py +++ b/flatcamTools/ToolPaint.py @@ -1254,32 +1254,38 @@ class ToolPaint(FlatCAMTool, Gerber): pass def paint_p(polyg, tooldia): - if paint_method == "seed": - # Type(cp) == FlatCAMRTreeStorage | None - cpoly = self.clear_polygon2(polyg, - tooldia=tooldia, - steps_per_circle=self.app.defaults["geometry_circle_steps"], - overlap=over, - contour=cont, - connect=conn) + cpoly = None + try: + if paint_method == "seed": + # Type(cp) == FlatCAMRTreeStorage | None + cpoly = self.clear_polygon2(polyg, + tooldia=tooldia, + steps_per_circle=self.app.defaults["geometry_circle_steps"], + overlap=over, + contour=cont, + connect=conn) - elif paint_method == "lines": - # Type(cp) == FlatCAMRTreeStorage | None - cpoly = self.clear_polygon3(polyg, - tooldia=tooldia, - steps_per_circle=self.app.defaults["geometry_circle_steps"], - overlap=over, - contour=cont, - connect=conn) + elif paint_method == "lines": + # Type(cp) == FlatCAMRTreeStorage | None + cpoly = self.clear_polygon3(polyg, + tooldia=tooldia, + steps_per_circle=self.app.defaults["geometry_circle_steps"], + overlap=over, + contour=cont, + connect=conn) - else: - # Type(cp) == FlatCAMRTreeStorage | None - cpoly = self.clear_polygon(polyg, - tooldia=tooldia, - steps_per_circle=self.app.defaults["geometry_circle_steps"], - overlap=over, - contour=cont, - connect=conn) + else: + # Type(cp) == FlatCAMRTreeStorage | None + cpoly = self.clear_polygon(polyg, + tooldia=tooldia, + steps_per_circle=self.app.defaults["geometry_circle_steps"], + overlap=over, + contour=cont, + connect=conn) + except FlatCAMApp.GracefulException: + return "fail" + except Exception as e: + log.debug("ToolPaint.paint_poly().gen_paintarea().paint_p() --> %s" % str(e)) if cpoly is not None: geo_obj.solid_geometry += list(cpoly.get_objects()) @@ -1326,13 +1332,15 @@ class ToolPaint(FlatCAMTool, Gerber): total_geometry += list(x.get_objects()) else: total_geometry = list(cp.get_objects()) + except FlatCAMApp.GracefulException: + 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))) - return + return "fail" # 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 @@ -1391,6 +1399,9 @@ class ToolPaint(FlatCAMTool, Gerber): def job_thread(app_obj): try: app_obj.new_object("geometry", name, gen_paintarea) + except FlatCAMApp.GracefulException: + proc.done() + return except Exception as e: proc.done() self.app.inform.emit('[ERROR_NOTCL] %s --> %s' % @@ -1498,6 +1509,9 @@ class ToolPaint(FlatCAMTool, Gerber): :param geometry: Shapely type or list or list of list of such. :param reset: Clears the contents of self.flat_geometry. """ + if self.app.abort_flag: + # graceful abort requested by the user + raise FlatCAMApp.GracefulException if geometry is None: return @@ -1611,13 +1625,15 @@ class ToolPaint(FlatCAMTool, Gerber): if cp is not None: total_geometry += list(cp.get_objects()) + except FlatCAMApp.GracefulException: + 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 All. Try a different combination of parameters. " "Or a different Method of paint"), str(e))) - return + return "fail" pol_nr += 1 disp_number = int(np.interp(pol_nr, [0, geo_len], [0, 99])) @@ -1741,14 +1757,15 @@ class ToolPaint(FlatCAMTool, Gerber): if cp is not None: cleared_geo += list(cp.get_objects()) - + except FlatCAMApp.GracefulException: + 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 All. Try a different combination of parameters. " "Or a different Method of paint"), str(e))) - return + return "fail" pol_nr += 1 disp_number = int(np.interp(pol_nr, [0, geo_len], [0, 99])) @@ -1803,6 +1820,9 @@ class ToolPaint(FlatCAMTool, Gerber): app_obj.new_object("geometry", name, gen_paintarea_rest_machining) else: app_obj.new_object("geometry", name, gen_paintarea) + except FlatCAMApp.GracefulException: + proc.done() + return except Exception as e: proc.done() traceback.print_stack() @@ -1896,6 +1916,9 @@ class ToolPaint(FlatCAMTool, Gerber): :param geometry: Shapely type or list or list of list of such. :param reset: Clears the contents of self.flat_geometry. """ + if self.app.abort_flag: + # graceful abort requested by the user + raise FlatCAMApp.GracefulException if geometry is None: return @@ -1991,6 +2014,7 @@ class ToolPaint(FlatCAMTool, Gerber): continue poly_buf = geo.buffer(-paint_margin) + cp = None if paint_method == "seed": # Type(cp) == FlatCAMRTreeStorage | None cp = self.clear_polygon2(poly_buf, @@ -2020,6 +2044,8 @@ class ToolPaint(FlatCAMTool, Gerber): if cp is not None: total_geometry += list(cp.get_objects()) + except FlatCAMApp.GracefulException: + return "fail" except Exception as e: log.debug("Could not Paint the polygons. %s" % str(e)) self.app.inform.emit('[ERROR] %s' % @@ -2149,7 +2175,8 @@ class ToolPaint(FlatCAMTool, Gerber): if cp is not None: cleared_geo += list(cp.get_objects()) - + except FlatCAMApp.GracefulException: + return "fail" except Exception as e: log.debug("Could not Paint the polygons. %s" % str(e)) self.app.inform.emit('[ERROR] %s' % @@ -2210,6 +2237,9 @@ class ToolPaint(FlatCAMTool, Gerber): app_obj.new_object("geometry", name, gen_paintarea_rest_machining) else: app_obj.new_object("geometry", name, gen_paintarea) + except FlatCAMApp.GracefulException: + proc.done() + return except Exception as e: proc.done() traceback.print_stack()