diff options
| author | Lex Neva <github.com@lexneva.name> | 2023-01-30 23:55:18 -0500 |
|---|---|---|
| committer | Lex Neva <github.com@lexneva.name> | 2023-02-16 22:54:21 -0500 |
| commit | b4612539585c1e9f4e75e0ad084da81f0ad758d3 (patch) | |
| tree | 75f68fb02a5106363bcd97e984dbe35a0d2c3dab | |
| parent | 30c73dc39c1ac814d0c63ec102c7f8c2b993fdd0 (diff) | |
make simulator threads pre-emptible
| -rw-r--r-- | lib/elements/fill_stitch.py | 3 | ||||
| -rw-r--r-- | lib/extensions/params.py | 5 | ||||
| -rw-r--r-- | lib/gui/simulator.py | 18 | ||||
| -rw-r--r-- | lib/stitches/auto_fill.py | 19 | ||||
| -rw-r--r-- | lib/stitches/fill.py | 3 | ||||
| -rw-r--r-- | lib/utils/threading.py | 21 |
6 files changed, 68 insertions, 1 deletions
diff --git a/lib/elements/fill_stitch.py b/lib/elements/fill_stitch.py index 7de293cb..eef8341c 100644 --- a/lib/elements/fill_stitch.py +++ b/lib/elements/fill_stitch.py @@ -22,6 +22,7 @@ from ..svg.tags import INKSCAPE_LABEL from ..utils import cache, version from .element import EmbroideryElement, param from .validation import ValidationError, ValidationWarning +from ..utils.threading import ExitThread class SmallShapeWarning(ValidationWarning): @@ -571,6 +572,8 @@ class FillStitch(EmbroideryElement): stitch_groups.extend(self.do_contour_fill(fill_shape, last_patch, start)) elif self.fill_method == 2: stitch_groups.extend(self.do_guided_fill(fill_shape, last_patch, start, end)) + except ExitThread: + raise except Exception: self.fatal_fill_error() last_patch = stitch_groups[-1] diff --git a/lib/extensions/params.py b/lib/extensions/params.py index 306e5e56..1262ceb6 100644 --- a/lib/extensions/params.py +++ b/lib/extensions/params.py @@ -25,6 +25,7 @@ from ..i18n import _ from ..svg.tags import SVG_POLYLINE_TAG from ..utils import get_resource_dir from .base import InkstitchExtension +from ..utils.threading import ExitThread, check_stop_flag def grouper(iterable_obj, count, fillvalue=None): @@ -509,9 +510,13 @@ class SettingsFrame(wx.Frame): # for many params in embroider.py. patches.extend(copy(node).embroider(None)) + + check_stop_flag() except SystemExit: wx.CallAfter(self._show_warning) raise + except ExitThread: + raise except Exception as e: # Ignore errors. This can be things like incorrect paths for # satins or division by zero caused by incorrect param values. diff --git a/lib/gui/simulator.py b/lib/gui/simulator.py index 1cc7066e..d9f51b48 100644 --- a/lib/gui/simulator.py +++ b/lib/gui/simulator.py @@ -10,6 +10,8 @@ from threading import Event, Thread import wx from wx.lib.intctrl import IntCtrl +from lib.debug import debug +from lib.utils.threading import ExitThread from ..i18n import _ from ..stitch_plan import stitch_groups_to_stitch_plan, stitch_plan_from_file from ..svg import PIXELS_PER_MM @@ -749,6 +751,10 @@ class SimulatorPreview(Thread): self.simulate_window = None self.refresh_needed = Event() + # This is read by utils.threading.check_stop_flag() to abort stitch plan + # generation. + self.stop = Event() + # used when closing to avoid having the window reopen at the last second self._disabled = False @@ -770,17 +776,27 @@ class SimulatorPreview(Thread): if not self.is_alive(): self.start() + self.stop.set() self.refresh_needed.set() def run(self): while True: self.refresh_needed.wait() self.refresh_needed.clear() - self.update_patches() + self.stop.clear() + + try: + debug.log("update_patches") + self.update_patches() + except ExitThread: + debug.log("ExitThread caught") + self.stop.clear() def update_patches(self): try: patches = self.parent.generate_patches(self.refresh_needed) + except ExitThread: + raise except: # noqa: E722 # If something goes wrong when rendering patches, it's not great, # but we don't really want the simulator thread to crash. Instead, diff --git a/lib/stitches/auto_fill.py b/lib/stitches/auto_fill.py index 3ff5a24f..fde3433a 100644 --- a/lib/stitches/auto_fill.py +++ b/lib/stitches/auto_fill.py @@ -19,6 +19,7 @@ from ..svg import PIXELS_PER_MM from ..utils.geometry import Point as InkstitchPoint, line_string_to_point_list, ensure_multi_line_string from .fill import intersect_region_with_grating, stitch_row from .running_stitch import running_stitch +from ..utils.threading import check_stop_flag class PathEdge(object): @@ -153,6 +154,8 @@ def build_fill_stitch_graph(shape, segments, starting_point=None, ending_point=N # mark this one as a grating segment. graph.add_edge(segment[0], segment[-1], key="segment", underpath_edges=[], geometry=shgeo.LineString(segment)) + check_stop_flag() + tag_nodes_with_outline_and_projection(graph, shape, graph.nodes()) add_edges_between_outline_nodes(graph, duplicate_every_other=True) @@ -196,6 +199,8 @@ def tag_nodes_with_outline_and_projection(graph, shape, nodes): graph.add_node(node, outline=outline_index, projection=outline_projection) + check_stop_flag() + def add_boundary_travel_nodes(graph, shape): outlines = ensure_multi_line_string(shape.boundary).geoms @@ -215,6 +220,8 @@ def add_boundary_travel_nodes(graph, shape): subpoint = segment.interpolate(i) graph.add_node((subpoint.x, subpoint.y), projection=outline.project(subpoint), outline=outline_index) + check_stop_flag() + graph.add_node((point.x, point.y), projection=outline.project(point), outline=outline_index) prev = point @@ -245,6 +252,8 @@ def add_edges_between_outline_nodes(graph, duplicate_every_other=False): if i % 2 == 0: graph.add_edge(node1, node2, key="extra", **data) + check_stop_flag() + def graph_is_valid(graph, shape, max_stitch_length): # The graph may be empty if the shape is so small that it fits between the @@ -382,6 +391,8 @@ def process_travel_edges(graph, fill_stitch_graph, shape, travel_edges): graph.add_edge(*edge, weight=weight) + check_stop_flag() + # without this, we sometimes get exceptions like this: # Exception AttributeError: "'NoneType' object has no attribute 'GEOSSTRtree_destroy'" in # <bound method STRtree.__del__ of <shapely.strtree.STRtree instance at 0x0D2BFD50>> ignored @@ -444,9 +455,13 @@ def build_travel_edges(shape, fill_angle): diagonal_edges = ensure_multi_line_string(grating1.symmetric_difference(grating2)) + check_stop_flag() + # without this, floating point inaccuracies prevent the intersection points from lining up perfectly. vertical_edges = ensure_multi_line_string(snap(grating3.difference(grating1), diagonal_edges, 0.005)) + check_stop_flag() + return endpoints, chain(diagonal_edges.geoms, vertical_edges.geoms) @@ -540,6 +555,8 @@ def find_stitch_path(graph, travel_graph, starting_point=None, ending_point=None real_end = nearest_node(outline_nodes, ending_point) path.append(PathEdge((ending_node, real_end), key="outline")) + check_stop_flag() + return path @@ -629,4 +646,6 @@ def path_to_stitches(path, travel_graph, fill_stitch_graph, angle, row_spacing, else: stitches.extend(travel(travel_graph, edge[0], edge[1], running_stitch_length, running_stitch_tolerance, skip_last)) + check_stop_flag() + return stitches diff --git a/lib/stitches/fill.py b/lib/stitches/fill.py index 11c9259b..b3bb0bb7 100644 --- a/lib/stitches/fill.py +++ b/lib/stitches/fill.py @@ -11,6 +11,7 @@ from ..stitch_plan import Stitch from ..svg import PIXELS_PER_MM from ..utils import Point as InkstitchPoint from ..utils import cache +from ..utils.threading import check_stop_flag def legacy_fill(shape, angle, row_spacing, end_row_spacing, max_stitch_length, flip, staggers, skip_last): @@ -169,6 +170,8 @@ def intersect_region_with_grating(shape, angle, row_spacing, end_row_spacing=Non else: current_row_y += row_spacing + check_stop_flag() + return rows diff --git a/lib/utils/threading.py b/lib/utils/threading.py new file mode 100644 index 00000000..7bb90d1b --- /dev/null +++ b/lib/utils/threading.py @@ -0,0 +1,21 @@ +import threading + +from ..exceptions import InkstitchException +from ..debug import debug + + +class ExitThread(InkstitchException): + """This exception is thrown in a thread to cause it to terminate. + + Presumably we should only catch this at the thread's top level. + """ + pass + + +_default_stop_flag = threading.Event() + + +def check_stop_flag(): + if getattr(threading.current_thread(), 'stop', _default_stop_flag).is_set(): + debug.log("exiting thread") + raise ExitThread() |
