kopia lustrzana https://github.com/inkstitch/inkstitch
303 wiersze
11 KiB
Python
303 wiersze
11 KiB
Python
# 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 = _get_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 _get_shortest_path(graph, starting_node, ending_node):
|
|
try:
|
|
path = nx.shortest_path(graph, starting_node, ending_node)
|
|
except nx.exception.NetworkXNoPath:
|
|
# They did something strange, let's add a jump from start to end
|
|
# so at least we don't have to end with an error
|
|
_insert_jump(graph, graph.nodes[starting_node]['point'], graph.nodes[ending_node]['point'])
|
|
path = nx.shortest_path(graph, starting_node, ending_node)
|
|
return 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.
|
|
last_element = None
|
|
for element1, element2 in zip(elements[:-1], elements[1:]):
|
|
# Try to fix missing links
|
|
if last_element is not None:
|
|
success = _insert_smallest_jump(graph, last_element, element2)
|
|
if success:
|
|
last_element = None
|
|
|
|
check_stop_flag()
|
|
success = _insert_smallest_jump(graph, element1, element2)
|
|
if not success:
|
|
last_element = element1
|
|
|
|
# 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)
|
|
return True
|
|
else:
|
|
# return False if we haven't successfully inserted a jump
|
|
# so we can try again later
|
|
return False
|
|
|
|
|
|
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 not nodes:
|
|
return list(graph.nodes)[0]
|
|
|
|
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, commands_only=False):
|
|
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'):
|
|
command_group.delete()
|
|
else:
|
|
command.connector.delete()
|
|
command.use.delete()
|
|
if not commands_only:
|
|
element.node.delete()
|
|
|
|
|
|
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, transform=True):
|
|
"""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 None:
|
|
continue
|
|
element_transform = ''
|
|
if transform:
|
|
element_transform = get_correction_transform(parent, child=True)
|
|
parent.append(element.node)
|
|
element.node.set('transform', element_transform)
|
|
element.node.apply_transform()
|
|
|
|
|
|
def add_elements_to_group(elements, group):
|
|
for element in elements:
|
|
group.append(element.node)
|
|
|
|
|
|
def apply_transform_recursivley(element, group_transform=inkex.Transform()):
|
|
"""Apply transform, helps to keep the stroke width"""
|
|
if element.TAG == 'g':
|
|
group_transform = group_transform @ element.transform
|
|
element.pop('transform')
|
|
for child in element:
|
|
apply_transform_recursivley(child, group_transform)
|
|
else:
|
|
element.transform = group_transform @ element.transform
|
|
element.apply_transform()
|