kopia lustrzana https://github.com/inkstitch/inkstitch
Merge pull request #2045 from inkstitch/lexelby/interruptible-threads
make simulator threads interruptiblepull/2081/head
commit
7aee0979be
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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']
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
Ładowanie…
Reference in New Issue