Merge pull request #2045 from inkstitch/lexelby/interruptible-threads

make simulator threads interruptible
pull/2081/head
Lex Neva 2023-02-16 22:58:10 -05:00 zatwierdzone przez GitHub
commit 7aee0979be
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
16 zmienionych plików z 130 dodań i 2 usunięć

Wyświetl plik

@ -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]

Wyświetl plik

@ -21,6 +21,7 @@ from ..utils import Point, cache, cut, cut_multiple, prng
from ..stitches import running_stitch
from .element import EmbroideryElement, param, PIXELS_PER_MM
from .validation import ValidationError, ValidationWarning
from ..utils.threading import check_stop_flag
class TooFewPathsError(ValidationError):
@ -818,10 +819,12 @@ class SatinColumn(EmbroideryElement):
index1 = 0
while index0 < last_index0 and index1 < last_index1:
check_stop_flag()
# Each iteration of this outer loop is one stitch. Keep going
# until we fall off the end of the section.
old_center = shgeo.Point(x/2 for x in (pos0 + pos1))
old_center = shgeo.Point(x / 2 for x in (pos0 + pos1))
while to_travel > 0 and index0 < last_index0 and index1 < last_index1:
# In this loop, we inch along each rail a tiny bit per

Wyświetl plik

@ -24,6 +24,7 @@ from ..svg.tags import (INKSCAPE_LABEL, INKSTITCH_LETTERING, SVG_GROUP_TAG,
from ..utils import DotDict, cache, get_bundled_dir, get_resource_dir
from .commands import CommandsExtension
from .lettering_custom_font_dir import get_custom_font_dir
from ..utils.threading import ExitThread
class LetteringFrame(wx.Frame):
@ -344,6 +345,8 @@ class LetteringFrame(wx.Frame):
patches.extend(element.embroider(None))
except SystemExit:
raise
except ExitThread:
raise
except Exception:
raise
# Ignore errors. This can be things like incorrect paths for

Wyświetl plik

@ -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.

Wyświetl plik

@ -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,

Wyświetl plik

@ -11,6 +11,7 @@ from ..i18n import _
from ..svg import PIXELS_PER_MM
from .color_block import ColorBlock
from .ties import add_ties
from ..utils.threading import check_stop_flag
def stitch_groups_to_stitch_plan(stitch_groups, collapse_len=None, min_stitch_len=0.1, disable_ties=False): # noqa: C901
@ -34,6 +35,8 @@ def stitch_groups_to_stitch_plan(stitch_groups, collapse_len=None, min_stitch_le
color_block = stitch_plan.new_color_block(color=stitch_groups[0].color)
for stitch_group in stitch_groups:
check_stop_flag()
if not stitch_group.stitches:
continue

Wyświetl plik

@ -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

Wyświetl plik

@ -21,6 +21,7 @@ from .utils.autoroute import (add_elements_to_group, add_jumps,
get_starting_and_ending_nodes,
preserve_original_groups,
remove_original_elements)
from ..utils.threading import check_stop_flag
class LineSegments:
@ -59,9 +60,13 @@ class LineSegments:
self._lines.append(line)
self._elements.append(element)
check_stop_flag()
def _get_intersection_points(self):
for i, line1 in enumerate(self._lines):
for j in range(i + 1, len(self._lines)):
check_stop_flag()
line2 = self._lines[j]
distance = line1.distance(line2)
if distance > 50:
@ -169,6 +174,8 @@ def build_graph(elements, preserve_order, break_up):
# any direction, so we add the edge in the opposite direction too.
graph.add_edge(str(end), str(start), element=element)
check_stop_flag()
return graph
@ -199,6 +206,8 @@ def path_to_elements(graph, path, trim): # noqa: C901
just_trimmed = False
el = None
for start, end, direction in path:
check_stop_flag()
element = graph[start][end].get('element')
start_coord = graph.nodes[start]['point']
end_coord = graph.nodes[end]['point']

Wyświetl plik

@ -24,6 +24,7 @@ from .utils.autoroute import (add_elements_to_group, add_jumps,
get_starting_and_ending_nodes,
preserve_original_groups,
remove_original_elements)
from ..utils.threading import check_stop_flag
class SatinSegment(object):
@ -366,6 +367,8 @@ def build_graph(elements, preserve_order=False):
# best spots for us.
for element in elements:
check_stop_flag()
segments = []
if isinstance(element, Stroke):
segments.append(RunningStitch(element))

Wyświetl plik

@ -15,6 +15,7 @@ from ..utils import DotDict
from ..utils.geometry import (cut, ensure_geometry_collection,
ensure_multi_polygon, reverse_line_string,
roll_linear_ring)
from ..utils.threading import check_stop_flag
from .running_stitch import running_stitch
@ -132,6 +133,8 @@ def offset_polygon(polygon, offset, join_style, clockwise):
active_holes[0].append(hole_node)
while len(active_polygons) > 0:
check_stop_flag()
current_poly = active_polygons.pop()
current_holes = active_holes.pop()
@ -325,6 +328,7 @@ def _find_path_inner_to_outer(tree, node, offset, starting_point, avoid_self_cro
Return value:
LineString -- the stitching path
"""
check_stop_flag()
current_node = tree.nodes[node]
current_ring = current_node.val
@ -473,6 +477,8 @@ def _check_and_prepare_tree_for_valid_spiral(tree):
"""
def process_node(node):
check_stop_flag()
children = set(tree[node])
if len(children) == 0:
@ -526,6 +532,8 @@ def _get_spiral_rings(tree):
node = 'root'
while True:
check_stop_flag()
rings.append(tree.nodes[node].val)
children = tree[node]
@ -556,6 +564,8 @@ def _make_spiral(rings, stitch_length, starting_point):
path = []
for ring1, ring2 in zip(rings[:-1], rings[1:]):
check_stop_flag()
spiral_part = _interpolate_linear_rings(ring1, ring2, stitch_length, starting_point)
path.extend(spiral_part.coords)

Wyświetl plik

@ -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):
@ -139,6 +140,8 @@ def intersect_region_with_grating(shape, angle, row_spacing, end_row_spacing=Non
current_row_y = start
rows = []
while current_row_y < end:
check_stop_flag()
p0 = center + normal * current_row_y + direction * half_length
p1 = center + normal * current_row_y - direction * half_length
endpoints = [p0.as_tuple(), p1.as_tuple()]
@ -169,6 +172,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

Wyświetl plik

@ -15,6 +15,7 @@ from ..utils.geometry import (ensure_geometry_collection,
from .auto_fill import (auto_fill, build_fill_stitch_graph, build_travel_graph,
collapse_sequential_outline_edges, find_stitch_path,
graph_is_valid, travel)
from ..utils.threading import check_stop_flag
def guided_fill(shape,
@ -68,6 +69,8 @@ def path_to_stitches(path, travel_graph, fill_stitch_graph, stitch_length, runni
stitches.append(Stitch(*path[0].nodes[0]))
for edge in path:
check_stop_flag()
if edge.is_segment():
current_edge = fill_stitch_graph[edge[0]][edge[-1]]['segment']
path_geometry = current_edge['geometry']
@ -233,6 +236,8 @@ def intersect_region_with_grating_guideline(shape, line, row_spacing, num_stagge
offset_line = None
rows = []
while True:
check_stop_flag()
if strategy == 0:
translate_amount = translate_direction * row * row_spacing
offset_line = translate(line, xoff=translate_amount.x, yoff=translate_amount.y)

Wyświetl plik

@ -9,6 +9,7 @@ from .running_stitch import running_stitch
from ..elements import SatinColumn
from ..utils import Point as InkstitchPoint
from ..utils.geometry import line_string_to_point_list
from ..utils.threading import check_stop_flag
def ripple_stitch(stroke):
@ -80,6 +81,8 @@ def _get_satin_ripple_helper_lines(stroke):
helper_lines = []
for point0, point1 in rail_pairs:
check_stop_flag()
helper_lines.append([])
helper_line = LineString((point0, point1))
for step in steps:
@ -95,6 +98,8 @@ def _converge_helper_line_points(helper_lines, point_edge=False):
num_lines = len(helper_lines)
steps = _get_steps(num_lines)
for i, line in enumerate(helper_lines):
check_stop_flag()
points = []
for j in range(len(line) - 1):
if point_edge and j % 2 == 1:
@ -133,6 +138,8 @@ def _target_point_helper_lines(stroke, outline):
target = stroke.get_ripple_target()
steps = _get_steps(stroke.get_line_count(), exponent=stroke.exponent, flip=stroke.flip_exponent)
for i, point in enumerate(outline.coords):
check_stop_flag()
line = LineString([point, target])
for step in steps:
@ -193,6 +200,8 @@ def _generate_guided_helper_lines(stroke, outline, max_distance, guide_line):
previous_guide_point = None
for i in range(stroke.get_line_count()):
check_stop_flag()
guide_point = InkstitchPoint.from_shapely_point(guide_line.interpolate(outline_steps[i], normalized=True))
translation = guide_point - start_point
scaling = scale_steps[i]
@ -232,6 +241,8 @@ def _generate_satin_guide_helper_lines(stroke, outline, guide_line):
# add scaled and rotated outlines along the satin column guide line
for i, (point0, point1) in enumerate(zip(*rail_points)):
check_stop_flag()
guide_center = (point0 + point1) / 2
translation = guide_center - outline_center
if stroke.rotate_ripples:

Wyświetl plik

@ -12,6 +12,7 @@ import numpy as np
from shapely import geometry as shgeo
from ..utils import prng
from ..utils.geometry import Point
from ..utils.threading import check_stop_flag
""" Utility functions to produce running stitches. """
@ -196,6 +197,8 @@ def stitch_curve_evenly(points: typing.Sequence[Point], stitch_length: float, to
last = points[0]
stitches = []
while i is not None and i < len(points):
check_stop_flag()
d = last.distance(points[i]) + distLeft[i]
if d == 0:
return stitches

Wyświetl plik

@ -13,6 +13,7 @@ import inkex
from ...svg import get_correction_transform
from ...svg.tags import INKSCAPE_LABEL
from ...utils.threading import check_stop_flag
def find_path(graph, starting_node, ending_node):
@ -31,6 +32,8 @@ def find_path(graph, starting_node, ending_node):
# "underpathing".
path = nx.shortest_path(graph, starting_node, ending_node)
check_stop_flag()
# Copy the graph so that we can remove the edges as we visit them.
# This also converts the directed graph into an undirected graph in the
# case that "preserve_order" is set.
@ -40,6 +43,8 @@ def find_path(graph, starting_node, ending_node):
final_path = []
prev = None
for node in path:
check_stop_flag()
if prev is not None:
final_path.append((prev, node))
prev = node
@ -85,6 +90,8 @@ def add_jumps(graph, elements, preserve_order):
# will enforce stitching the elements in order.
for element1, element2 in zip(elements[:-1], elements[1:]):
check_stop_flag()
potential_edges = []
nodes1 = get_nodes_on_element(graph, element1)
@ -106,6 +113,7 @@ def add_jumps(graph, elements, preserve_order):
# a weight, which we'll set as the length of the jump stitch. The
# algorithm will minimize the total length of jump stitches added.
for jump in nx.k_edge_augmentation(graph, 1, avail=list(possible_jumps(graph))):
check_stop_flag()
graph.add_edge(*jump, jump=True)
return graph

Wyświetl plik

@ -0,0 +1,22 @@
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
# A default flag used for the main thread. It will never be set.
_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()