pull/409/head
Lex Neva 2019-03-14 21:02:47 -04:00
rodzic 30ea54dc6d
commit e616061e85
2 zmienionych plików z 59 dodań i 82 usunięć

Wyświetl plik

@ -1,14 +1,14 @@
from collections import deque from itertools import groupby, chain
from itertools import groupby, izip import math
import sys
import networkx import networkx
from shapely import geometry as shgeo from shapely import geometry as shgeo
from ..exceptions import InkstitchException from ..exceptions import InkstitchException
from ..i18n import _ from ..i18n import _
from ..svg import PIXELS_PER_MM
from ..utils.geometry import Point as InkstitchPoint, cut from ..utils.geometry import Point as InkstitchPoint, cut
from .fill import intersect_region_with_grating, row_num, stitch_row from .fill import intersect_region_with_grating, stitch_row
from .running_stitch import running_stitch from .running_stitch import running_stitch
@ -50,13 +50,16 @@ def auto_fill(shape,
staggers, staggers,
skip_last, skip_last,
starting_point, starting_point,
ending_point=None): ending_point=None,
underpath=True):
graph = build_graph(shape, angle, row_spacing, end_row_spacing) graph = build_graph(shape, angle, row_spacing, end_row_spacing)
check_graph(graph, shape, max_stitch_length) check_graph(graph, shape, max_stitch_length)
travel_graph = build_travel_graph(graph, shape, angle, underpath)
path = find_stitch_path(graph, starting_point, ending_point) path = find_stitch_path(graph, starting_point, ending_point)
result = path_to_stitches(path, graph, travel_graph, shape, angle, row_spacing, max_stitch_length, running_stitch_length, staggers, skip_last)
return path_to_stitches(path, graph, shape, angle, row_spacing, max_stitch_length, running_stitch_length, staggers, skip_last) return result
def which_outline(shape, coords): def which_outline(shape, coords):
@ -156,6 +159,48 @@ def build_graph(shape, angle, row_spacing, end_row_spacing):
return graph return graph
def build_travel_graph(top_stitch_graph, shape, top_stitch_angle, underpath):
graph = networkx.Graph()
graph.add_nodes_from(top_stitch_graph.nodes(data=True))
if underpath:
# need to concatenate all the rows
grating1 = shgeo.MultiLineString(list(chain(*intersect_region_with_grating(shape, top_stitch_angle + math.pi / 4, 2 * PIXELS_PER_MM))))
grating2 = shgeo.MultiLineString(list(chain(*intersect_region_with_grating(shape, top_stitch_angle - math.pi / 4, 2 * PIXELS_PER_MM))))
endpoints = [coord for mls in (grating1, grating2)
for ls in mls
for coord in ls.coords]
for node in endpoints:
outline_index = which_outline(shape, node)
outline_projection = project(shape, node, outline_index)
# Tag each node with its index and projection.
graph.add_node(node, index=outline_index, projection=outline_projection)
nodes = list(graph.nodes(data=True)) # returns a list of tuples: [(node, {data}), (node, {data}) ...]
nodes.sort(key=lambda node: (node[1]['index'], node[1]['projection']))
for outline_index, nodes in groupby(nodes, key=lambda node: node[1]['index']):
nodes = [node for node, data in nodes]
# add an edge between each successive node
for node1, node2 in zip(nodes, nodes[1:] + [nodes[0]]):
p1 = InkstitchPoint(*node1)
p2 = InkstitchPoint(*node2)
graph.add_edge(node1, node2, weight=3 * p1.distance(p2))
if underpath:
interior_edges = grating1.symmetric_difference(grating2)
for ls in interior_edges.geoms:
p1, p2 = [InkstitchPoint(*coord) for coord in ls.coords]
graph.add_edge(p1.as_tuple(), p2.as_tuple(), weight=p1.distance(p2))
return graph
def check_graph(graph, shape, max_stitch_length): def check_graph(graph, shape, max_stitch_length):
if networkx.is_empty(graph) or not networkx.is_eulerian(graph): if networkx.is_empty(graph) or not networkx.is_eulerian(graph):
if shape.area < max_stitch_length ** 2: if shape.area < max_stitch_length ** 2:
@ -285,86 +330,18 @@ def collapse_sequential_outline_edges(path):
return new_path return new_path
def connect_points(graph, shape, start, end, running_stitch_length, row_spacing): def travel(graph, travel_graph, shape, start, end, running_stitch_length, row_spacing):
"""Create stitches to get from one point on an outline of the shape to another. """Create stitches to get from one point on an outline of the shape to another."""
An outline is essentially a loop (a path of points that ends where it starts). path = networkx.shortest_path(travel_graph, start, end, weight='weight')
Given point A and B on that loop, we want to take the shortest path from one path = [InkstitchPoint(*p) for p in path]
to the other. Due to the way our path-finding algorithm above works, it may
have had to take the long way around the shape to get from A to B, but we'd
rather ignore that and just get there the short way.
"""
# We may be on the outer boundary or on on of the hole boundaries.
outline_index = graph.nodes[start]['index']
outline = shape.boundary[outline_index]
# First, figure out the start and end position along the outline. The
# projection gives us the distance travelled down the outline to get to
# that point.
start_projection = graph.nodes[start]['projection']
start = shgeo.Point(start)
end_projection = graph.nodes[end]['projection']
end = shgeo.Point(end)
# If the points are pretty close, just jump there. There's a slight
# risk that we're going to sew outside the shape here. The way to
# avoid that is to use running_stitch() even for these really short
# connections, but that would be really slow for all of the
# connections from one row to the next.
#
# This seems to do a good job of avoiding going outside the shape in
# most cases. 1.4 is chosen as approximately the length of the
# stitch connecting two rows if the side of the shape is at a 45
# degree angle to the rows of stitches (sqrt(2)).
direct_distance = abs(end_projection - start_projection)
if direct_distance < row_spacing * 1.4 and direct_distance < running_stitch_length:
return [InkstitchPoint(end.x, end.y)]
# The outline path has a "natural" starting point. Think of this as
# 0 or 12 on an analog clock.
# Cut the outline into two paths at the starting point. The first
# section will go from 12 o'clock to the starting point. The second
# section will go from the starting point all the way around and end
# up at 12 again.
result = cut(outline, start_projection)
# result[0] will be None if our starting point happens to already be at
# 12 o'clock.
if result[0] is not None:
before, after = result
# Make a new outline, starting from the starting point. This is
# like rotating the clock so that now our starting point is
# at 12 o'clock.
outline = shgeo.LineString(list(after.coords) + list(before.coords))
# Now figure out where our ending point is on the newly-rotated clock.
end_projection = outline.project(end)
# Cut the new path at the ending point. before and after now represent
# two ways to get from the starting point to the ending point. One
# will most likely be longer than the other.
before, after = cut(outline, end_projection)
if before.length <= after.length:
points = list(before.coords)
else:
# after goes from the ending point to the starting point, so reverse
# it to get from start to end.
points = list(reversed(after.coords))
# Now do running stitch along the path we've found. running_stitch() will
# avoid cutting sharp corners.
path = [InkstitchPoint(*p) for p in points]
stitches = running_stitch(path, running_stitch_length) stitches = running_stitch(path, running_stitch_length)
# The row of stitches already stitched the first point, so skip it. # The row of stitches already stitched the first point, so skip it.
return stitches[1:] return stitches[1:]
def path_to_stitches(path, graph, shape, angle, row_spacing, max_stitch_length, running_stitch_length, staggers, skip_last): def path_to_stitches(path, graph, travel_graph, shape, angle, row_spacing, max_stitch_length, running_stitch_length, staggers, skip_last):
path = collapse_sequential_outline_edges(path) path = collapse_sequential_outline_edges(path)
stitches = [] stitches = []
@ -373,6 +350,6 @@ def path_to_stitches(path, graph, shape, angle, row_spacing, max_stitch_length,
if edge.is_segment(): if edge.is_segment():
stitch_row(stitches, edge[0], edge[1], angle, row_spacing, max_stitch_length, staggers, skip_last) stitch_row(stitches, edge[0], edge[1], angle, row_spacing, max_stitch_length, staggers, skip_last)
else: else:
stitches.extend(connect_points(graph, shape, edge[0], edge[1], running_stitch_length, row_spacing)) stitches.extend(travel(graph, travel_graph, shape, edge[0], edge[1], running_stitch_length, row_spacing))
return stitches return stitches

Wyświetl plik

@ -140,7 +140,7 @@ def intersect_region_with_grating(shape, angle, row_spacing, end_row_spacing=Non
res = grating_line.intersection(shape) res = grating_line.intersection(shape)
if (isinstance(res, shapely.geometry.MultiLineString)): if (isinstance(res, shapely.geometry.MultiLineString)):
runs = map(lambda line_string: line_string.coords, res.geoms) runs = [line_string.coords for line_string in res.geoms]
else: else:
if res.is_empty or len(res.coords) == 1: if res.is_empty or len(res.coords) == 1:
# ignore if we intersected at a single point or no points # ignore if we intersected at a single point or no points
@ -153,7 +153,7 @@ def intersect_region_with_grating(shape, angle, row_spacing, end_row_spacing=Non
if flip: if flip:
runs.reverse() runs.reverse()
runs = map(lambda run: tuple(reversed(run)), runs) runs = [tuple(reversed(run)) for run in runs]
rows.append(runs) rows.append(runs)