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
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
|
# Authors: see git history
#
# Copyright (c) 2010 Authors
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
from itertools import combinations
import inkex
import networkx as nx
from shapely.geometry import MultiPoint, Point
from shapely.ops import nearest_points
from ...elements import SatinColumn
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):
"""Find a path through the graph that sews every edge."""
# This is done in two steps. First, we find the shortest path from the
# start to the end. We remove it from the graph, and proceed to step 2.
#
# Then, we traverse the path node by node. At each node, we follow any
# branchings with a depth-first search. We travel down each branch of
# the tree, inserting each seen branch into the tree. When the DFS
# hits a dead-end, as it back-tracks, we also add the seen edges _again_.
# Repeat until there are no more edges left in the graph.
#
# Visiting the edges again on the way back allows us to set up
# "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.
graph = nx.Graph(graph)
graph.remove_edges_from(list(zip(path[:-1], path[1:])))
final_path = []
prev = None
for node in path:
check_stop_flag()
if prev is not None:
final_path.append((prev, node))
prev = node
for n1, n2, edge_type in list(nx.dfs_labeled_edges(graph, node)):
if n1 == n2:
# dfs_labeled_edges gives us (start, start, "forward") for
# the starting node for some reason
continue
if edge_type == "forward":
final_path.append((n1, n2))
graph.remove_edge(n1, n2)
elif edge_type == "reverse":
final_path.append((n2, n1))
elif edge_type == "nontree":
# a "nontree" happens when there exists an edge from n1 to n2
# but n2 has already been visited. It's a dead-end that runs
# into part of the graph that we've already traversed. We
# do still need to make sure that edge is sewn, so we travel
# down and back on this edge.
#
# It's possible for a given "nontree" edge to be listed more
# than once so we'll deduplicate.
if (n1, n2) in graph.edges:
final_path.append((n1, n2))
final_path.append((n2, n1))
graph.remove_edge(n1, n2)
return final_path
def add_jumps(graph, elements, preserve_order):
"""Add jump stitches between elements as necessary.
Jump stitches are added to ensure that all elements can be reached. Only the
minimal number and length of jumps necessary will be added.
"""
if preserve_order:
_add_ordered_jumps(graph, elements)
else:
_add_unordered_jumps(graph, elements)
return graph
def _add_ordered_jumps(graph, elements):
# For each sequential pair of elements, find the shortest possible jump
# stitch between them and add it. The directions of these new edges
# will enforce stitching the elements in order.
for element1, element2 in zip(elements[:-1], elements[1:]):
check_stop_flag()
_insert_smallest_jump(graph, element1, element2)
# add jumps between subpath too, we do not care about directions here
for element in elements:
if isinstance(element, SatinColumn):
# don't try this for satin columns
continue
check_stop_flag()
geoms = list(element.as_multi_line_string().geoms)
i = 0
for line1 in geoms:
for line2 in geoms[i+1:]:
if line1.distance(line2) == 0:
continue
node1, node2 = nearest_points(line1, line2)
_insert_jump(graph, node1, node2)
i += 1
def _insert_smallest_jump(graph, element1, element2):
potential_edges = []
nodes1 = get_nodes_on_element(graph, element1)
nodes2 = get_nodes_on_element(graph, element2)
for node1 in nodes1:
for node2 in nodes2:
point1 = graph.nodes[node1]['point']
point2 = graph.nodes[node2]['point']
potential_edges.append((point1, point2))
if potential_edges:
edge = min(potential_edges, key=lambda p1_p2: p1_p2[0].distance(p1_p2[1]))
graph.add_edge(str(edge[0]), str(edge[1]), jump=True)
def _insert_jump(graph, node1, node2):
graph.add_node(str(node1), point=node1)
graph.add_node(str(node2), point=node2)
graph.add_edge(str(node1), str(node2), jump=True)
graph.add_edge(str(node2), str(node1), jump=True)
def _add_unordered_jumps(graph, elements):
# networkx makes this super-easy! k_edge_agumentation tells us what edges
# we need to add to ensure that the graph is fully connected. We give it a
# set of possible edges that it can consider adding (avail). Each edge has
# 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)
def possible_jumps(graph):
"""All possible jump stitches in the graph with their lengths.
Returns: a generator of tuples: (node1, node2, length)
"""
for component1, component2 in combinations(nx.connected_components(graph), 2):
points1 = MultiPoint([graph.nodes[node]['point'] for node in component1])
points2 = MultiPoint([graph.nodes[node]['point'] for node in component2])
start_point, end_point = nearest_points(points1, points2)
yield (str(start_point), str(end_point), start_point.distance(end_point))
def get_starting_and_ending_nodes(graph, elements, preserve_order, starting_point, ending_point):
"""Find or choose the starting and ending graph nodes.
If points were passed, we'll find the nearest graph nodes. Since we split
every path up into 1mm-chunks, we'll be at most 1mm away which is good
enough.
If we weren't given starting and ending points, we'll pic kthe far left and
right nodes.
returns:
(starting graph node, ending graph node)
"""
nodes = []
nodes.append(find_node(graph, starting_point,
min, preserve_order, elements[0]))
nodes.append(find_node(graph, ending_point,
max, preserve_order, elements[-1]))
return nodes
def find_node(graph, point, extreme_function, constrain_to_satin=False, satin=None):
if constrain_to_satin:
nodes = get_nodes_on_element(graph, satin)
else:
nodes = graph.nodes()
if point is None:
return extreme_function(nodes, key=lambda node: graph.nodes[node]['point'].x)
else:
point = Point(*point)
return min(nodes, key=lambda node: graph.nodes[node]['point'].distance(point))
def get_nodes_on_element(graph, element):
nodes = set()
for start_node, end_node, element_for_edge in graph.edges(data='element'):
if element_for_edge is element:
nodes.add(start_node)
nodes.add(end_node)
return nodes
def remove_original_elements(elements):
for element in elements:
for command in element.commands:
command_group = command.use.getparent()
if command_group is not None and command_group.get('id').startswith('command_group'):
remove_from_parent(command_group)
else:
remove_from_parent(command.connector)
remove_from_parent(command.use)
remove_from_parent(element.node)
def remove_from_parent(node):
if node.getparent() is not None:
node.getparent().remove(node)
def create_new_group(parent, insert_index, label, correction_transform=True):
group = inkex.Group(attrib={
INKSCAPE_LABEL: label,
})
if correction_transform:
group.transform = get_correction_transform(parent, child=True)
parent.insert(insert_index, group)
return group
def preserve_original_groups(elements, original_parent_nodes):
"""Ensure that all elements are contained in the original SVG group elements.
When preserve_order is True, no elements will be reordered in the XML tree.
This makes it possible to preserve original SVG group membership. We'll
ensure that each newly-created element is added to the group that contained
the original element that spawned it.
"""
for element, parent in zip(elements, original_parent_nodes):
if parent is not None:
parent.append(element.node)
element.node.set('transform', get_correction_transform(parent, child=True))
def add_elements_to_group(elements, group):
for element in elements:
group.append(element.node)
|