1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
|
from shapely import geometry as shgeo
from shapely.ops import linemerge, unary_union
from .auto_fill import (build_fill_stitch_graph,
build_travel_graph, collapse_sequential_outline_edges, fallback,
find_stitch_path, graph_is_valid, travel)
from .running_stitch import running_stitch
from ..debug import debug
from ..i18n import _
from ..stitch_plan import Stitch
from ..utils.geometry import Point as InkstitchPoint, reverse_line_string
def guided_fill(shape,
guideline,
angle,
row_spacing,
max_stitch_length,
running_stitch_length,
skip_last,
starting_point,
ending_point=None,
underpath=True):
try:
segments = intersect_region_with_grating_guideline(shape, guideline, row_spacing)
fill_stitch_graph = build_fill_stitch_graph(shape, segments, starting_point, ending_point)
except ValueError:
# Small shapes will cause the graph to fail - min() arg is an empty sequence through insert node
return fallback(shape, running_stitch_length)
if not graph_is_valid(fill_stitch_graph, shape, max_stitch_length):
return fallback(shape, running_stitch_length)
travel_graph = build_travel_graph(fill_stitch_graph, shape, angle, underpath)
path = find_stitch_path(fill_stitch_graph, travel_graph, starting_point, ending_point)
result = path_to_stitches(path, travel_graph, fill_stitch_graph, max_stitch_length, running_stitch_length, skip_last)
return result
def path_to_stitches(path, travel_graph, fill_stitch_graph, stitch_length, running_stitch_length, skip_last):
path = collapse_sequential_outline_edges(path)
stitches = []
# If the very first stitch is travel, we'll omit it in travel(), so add it here.
if not path[0].is_segment():
stitches.append(Stitch(*path[0].nodes[0]))
for edge in path:
if edge.is_segment():
current_edge = fill_stitch_graph[edge[0]][edge[-1]]['segment']
path_geometry = current_edge['geometry']
if edge[0] != path_geometry.coords[0]:
path_geometry = reverse_line_string(path_geometry)
point_list = [Stitch(*point) for point in path_geometry.coords]
new_stitches = running_stitch(point_list, stitch_length)
# need to tag stitches
if skip_last:
del new_stitches[-1]
stitches.extend(new_stitches)
travel_graph.remove_edges_from(fill_stitch_graph[edge[0]][edge[1]]['segment'].get('underpath_edges', []))
else:
stitches.extend(travel(travel_graph, edge[0], edge[1], running_stitch_length, skip_last))
return stitches
def extend_line(line, minx, maxx, miny, maxy):
line = line.simplify(0.01, False)
upper_left = InkstitchPoint(minx, miny)
lower_right = InkstitchPoint(maxx, maxy)
length = (upper_left - lower_right).length()
point1 = InkstitchPoint(*line.coords[0])
point2 = InkstitchPoint(*line.coords[1])
new_starting_point = point1 - (point2 - point1).unit() * length
point3 = InkstitchPoint(*line.coords[-2])
point4 = InkstitchPoint(*line.coords[-1])
new_ending_point = point4 + (point4 - point3).unit() * length
return shgeo.LineString([new_starting_point.as_tuple()] +
line.coords[1:-1] + [new_ending_point.as_tuple()])
def repair_multiple_parallel_offset_curves(multi_line):
lines = linemerge(multi_line)
lines = list(lines.geoms)
max_length = -1
max_length_idx = -1
for idx, subline in enumerate(lines):
if subline.length > max_length:
max_length = subline.length
max_length_idx = idx
# need simplify to avoid doubled points caused by linemerge
return lines[max_length_idx].simplify(0.01, False)
def repair_non_simple_lines(line):
repaired = unary_union(line)
counter = 0
# Do several iterations since we might have several concatenated selfcrossings
while repaired.geom_type != 'LineString' and counter < 4:
line_segments = []
for line_seg in repaired.geoms:
if not line_seg.is_ring:
line_segments.append(line_seg)
repaired = unary_union(linemerge(line_segments))
counter += 1
if repaired.geom_type != 'LineString':
raise ValueError(
_("Guide line (or offset copy) is self crossing!"))
else:
return repaired
def intersect_region_with_grating_guideline(shape, line, row_spacing, flip=False): # noqa: C901
row_spacing = abs(row_spacing)
(minx, miny, maxx, maxy) = shape.bounds
upper_left = InkstitchPoint(minx, miny)
rows = []
if line.geom_type != 'LineString' or not line.is_simple:
line = repair_non_simple_lines(line)
# extend the line towards the ends to increase probability that all offsetted curves cross the shape
line = extend_line(line, minx, maxx, miny, maxy)
line_offsetted = line
res = line_offsetted.intersection(shape)
while isinstance(res, (shgeo.GeometryCollection, shgeo.MultiLineString)) or (not res.is_empty and len(res.coords) > 1):
if isinstance(res, (shgeo.GeometryCollection, shgeo.MultiLineString)):
runs = [line_string.coords for line_string in res.geoms if (
not line_string.is_empty and len(line_string.coords) > 1)]
else:
runs = [res.coords]
runs.sort(key=lambda seg: (
InkstitchPoint(*seg[0]) - upper_left).length())
if flip:
runs.reverse()
runs = [tuple(reversed(run)) for run in runs]
if row_spacing > 0:
rows.append(runs)
else:
rows.insert(0, runs)
line_offsetted = line_offsetted.parallel_offset(row_spacing, 'left', 5)
if line_offsetted.geom_type == 'MultiLineString': # if we got multiple lines take the longest
line_offsetted = repair_multiple_parallel_offset_curves(line_offsetted)
if not line_offsetted.is_simple:
line_offsetted = repair_non_simple_lines(line_offsetted)
if row_spacing < 0:
line_offsetted = reverse_line_string(line_offsetted)
line_offsetted = line_offsetted.simplify(0.01, False)
res = line_offsetted.intersection(shape)
if row_spacing > 0 and not isinstance(res, (shgeo.GeometryCollection, shgeo.MultiLineString)):
if (res.is_empty or len(res.coords) == 1):
row_spacing = -row_spacing
line_offsetted = line.parallel_offset(row_spacing, 'left', 5)
if line_offsetted.geom_type == 'MultiLineString': # if we got multiple lines take the longest
line_offsetted = repair_multiple_parallel_offset_curves(
line_offsetted)
if not line_offsetted.is_simple:
line_offsetted = repair_non_simple_lines(line_offsetted)
# using negative row spacing leads as a side effect to reversed offsetted lines - here we undo this
line_offsetted = reverse_line_string(line_offsetted)
line_offsetted = line_offsetted.simplify(0.01, False)
res = line_offsetted.intersection(shape)
for row in rows:
yield from row
|