summaryrefslogtreecommitdiff
path: root/lib/stitches/meander_fill.py
diff options
context:
space:
mode:
Diffstat (limited to 'lib/stitches/meander_fill.py')
-rw-r--r--lib/stitches/meander_fill.py177
1 files changed, 177 insertions, 0 deletions
diff --git a/lib/stitches/meander_fill.py b/lib/stitches/meander_fill.py
new file mode 100644
index 00000000..964a7a41
--- /dev/null
+++ b/lib/stitches/meander_fill.py
@@ -0,0 +1,177 @@
+from itertools import combinations
+
+import networkx as nx
+from shapely.geometry import MultiPoint, Point
+from shapely.ops import nearest_points
+
+from .running_stitch import running_stitch
+from .. import tiles
+from ..debug import debug
+from ..utils.clamp_path import clamp_path_to_polygon
+from ..utils.geometry import Point as InkStitchPoint, ensure_geometry_collection
+from ..utils.list import poprandom
+from ..utils.prng import iter_uniform_floats
+from ..utils.smoothing import smooth_path
+from ..utils.threading import check_stop_flag
+
+
+def meander_fill(fill, shape, shape_index, starting_point, ending_point):
+ debug.log(f"meander pattern: {fill.meander_pattern}")
+ tile = get_tile(fill.meander_pattern)
+ if not tile:
+ return []
+
+ debug.log(f"tile name: {tile.name}")
+
+ debug.log_line_strings(lambda: ensure_geometry_collection(shape.boundary).geoms, 'Meander shape')
+ graph = tile.to_graph(shape, fill.meander_scale)
+ debug.log_graph(graph, 'Meander graph')
+ ensure_connected(graph)
+ start, end = find_starting_and_ending_nodes(graph, shape, starting_point, ending_point)
+ rng = iter_uniform_floats(fill.random_seed, 'meander-fill', shape_index)
+
+ return post_process(generate_meander_path(graph, start, end, rng), shape, fill)
+
+
+def get_tile(tile_id):
+ all_tiles = {tile.id: tile for tile in tiles.all_tiles()}
+
+ try:
+ return all_tiles.get(tile_id, all_tiles.popitem()[1])
+ except KeyError:
+ return None
+
+
+def ensure_connected(graph):
+ """If graph is unconnected, add edges to make it connected."""
+
+ # TODO: combine this with possible_jumps() in lib/stitches/utils/autoroute.py
+ possible_connections = []
+ for component1, component2 in combinations(nx.connected_components(graph), 2):
+ points1 = MultiPoint([Point(node) for node in component1])
+ points2 = MultiPoint([Point(node) for node in component2])
+
+ start_point, end_point = nearest_points(points1, points2)
+ possible_connections.append(((start_point.x, start_point.y), (end_point.x, end_point.y), start_point.distance(end_point)))
+
+ if possible_connections:
+ for start, end in nx.k_edge_augmentation(graph, 1, avail=possible_connections):
+ check_stop_flag()
+ graph.add_edge(start, end)
+
+
+def find_starting_and_ending_nodes(graph, shape, starting_point, ending_point):
+ if starting_point is None:
+ starting_point = shape.exterior.coords[0]
+ starting_point = Point(starting_point)
+
+ if ending_point is None:
+ ending_point = starting_point
+ else:
+ ending_point = Point(ending_point)
+
+ all_points = MultiPoint(list(graph))
+
+ starting_node = nearest_points(starting_point, all_points)[1].coords[0]
+ ending_node = nearest_points(ending_point, all_points)[1].coords[0]
+
+ if starting_node == ending_node:
+ # We need a path to start with, so pick a new ending node
+ all_points = all_points.difference(Point(starting_node))
+ ending_node = nearest_points(ending_point, all_points)[1].coords[0]
+
+ return starting_node, ending_node
+
+
+def find_initial_path(graph, start, end):
+ # We need some path to start with. We could use
+ # nx.all_simple_paths(graph, start, end) and choose the first one.
+ # However, that tends to pick a really "orderly" path. Shortest
+ # path looks more random.
+
+ # TODO: handle if this can't find a path
+ return nx.shortest_path(graph, start, end)
+
+
+@debug.time
+def generate_meander_path(graph, start, end, rng):
+ path = find_initial_path(graph, start, end)
+ path_edges = list(zip(path[:-1], path[1:]))
+ graph.remove_edges_from(path_edges)
+ graph_nodes = set(graph) - set(path)
+
+ edges_to_consider = list(path_edges)
+ meander_path = path_edges
+ while edges_to_consider:
+ while edges_to_consider:
+ check_stop_flag()
+
+ edge = poprandom(edges_to_consider, rng)
+ edges_to_consider.extend(replace_edge(meander_path, edge, graph, graph_nodes))
+
+ edge_pairs = list(zip(meander_path[:-1], meander_path[1:]))
+ while edge_pairs:
+ check_stop_flag()
+
+ edge1, edge2 = poprandom(edge_pairs, rng)
+ edges_to_consider.extend(replace_edge_pair(meander_path, edge1, edge2, graph, graph_nodes))
+ break
+
+ return path_to_points(meander_path)
+
+
+def replace_edge(path, edge, graph, graph_nodes):
+ subgraph = graph.subgraph(graph_nodes | set(edge))
+ new_path = None
+ for new_path in nx.all_simple_edge_paths(subgraph, edge[0], edge[1], 7):
+ if len(new_path) > 1:
+ break
+ if new_path is None or len(new_path) == 1:
+ return []
+ i = path.index(edge)
+ path[i:i + 1] = new_path
+ graph.remove_edges_from(new_path)
+ # do I need to remove the last one too?
+ graph_nodes.difference_update(start for start, end in new_path)
+ # debug.log(f"found new path of length {len(new_path)} at position {i}")
+
+ return new_path
+
+
+def replace_edge_pair(path, edge1, edge2, graph, graph_nodes):
+ subgraph = graph.subgraph(graph_nodes | {edge1[0], edge2[1]})
+ new_path = None
+ for new_path in nx.all_simple_edge_paths(subgraph, edge1[0], edge2[1], 10):
+ if len(new_path) > 2:
+ break
+ if new_path is None or len(new_path) <= 2:
+ return []
+ i = path.index(edge1)
+ path[i:i + 2] = new_path
+ graph.remove_edges_from(new_path)
+ # do I need to remove the last one too?
+ graph_nodes.difference_update(start for start, end in new_path)
+ # debug.log(f"found new pair path of length {len(new_path)} at position {i}")
+
+ return new_path
+
+
+@debug.time
+def post_process(points, shape, fill):
+ debug.log(f"smoothness: {fill.smoothness}")
+ # debug.log_line_string(LineString(points), "pre-smoothed", "#FF0000")
+ smoothed_points = smooth_path(points, fill.smoothness)
+ smoothed_points = [InkStitchPoint.from_tuple(point) for point in smoothed_points]
+
+ stitches = running_stitch(smoothed_points, fill.running_stitch_length, fill.running_stitch_tolerance)
+ stitches = clamp_path_to_polygon(stitches, shape)
+
+ return stitches
+
+
+def path_to_points(path):
+ points = [start for start, end in path]
+ if path:
+ points.append(path[-1][1])
+
+ return points