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()