|
| 1 | +# Authors: see git history |
| 2 | +# |
| 3 | +# Copyright (c) 2010 Authors |
| 4 | +# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. |
| 5 | + |
| 6 | +# -*- coding: UTF-8 -*- |
| 7 | + |
| 8 | +from math import sqrt |
| 9 | + |
| 10 | +import networkx |
| 11 | +from shapely import line_merge, prepare, snap |
| 12 | +from shapely.affinity import rotate, scale, translate |
| 13 | +from shapely.geometry import (LineString, MultiLineString, MultiPoint, Point, |
| 14 | + Polygon) |
| 15 | +from shapely.ops import nearest_points, unary_union |
| 16 | + |
| 17 | +from ..debug.debug import debug |
| 18 | +from ..stitch_plan import Stitch |
| 19 | +from ..utils.clamp_path import clamp_path_to_polygon |
| 20 | +from ..utils.geometry import Point as InkstitchPoint |
| 21 | +from ..utils.geometry import ensure_multi_line_string, reverse_line_string |
| 22 | +from ..utils.threading import check_stop_flag |
| 23 | +from .auto_fill import (add_edges_between_outline_nodes, |
| 24 | + build_fill_stitch_graph, fallback, find_stitch_path, |
| 25 | + graph_make_valid, process_travel_edges, |
| 26 | + tag_nodes_with_outline_and_projection) |
| 27 | +from .circular_fill import _apply_bean_stitch_and_repeats |
| 28 | + |
| 29 | + |
| 30 | +@debug.time |
| 31 | +def cross_stitch(fill, shape, starting_point, ending_point): |
| 32 | + stitch_length = fill.max_stitch_length |
| 33 | + |
| 34 | + square_size = stitch_length / sqrt(2) # 45° angle |
| 35 | + square = Polygon([(0, 0), (square_size, 0), (square_size, square_size), (0, square_size)]) |
| 36 | + full_square_area = square.area |
| 37 | + |
| 38 | + # start and end have to be a multiple of the stitch length |
| 39 | + minx, miny, maxx, maxy = shape.bounds |
| 40 | + adapted_minx = minx - minx % square_size |
| 41 | + adapted_miny = miny - miny % square_size |
| 42 | + adapted_maxx = maxx + square_size - maxx % square_size |
| 43 | + adapted_maxy = maxy + square_size - maxy % square_size |
| 44 | + prepare(shape) |
| 45 | + |
| 46 | + crosses_lr = [] |
| 47 | + crosses_rl = [] |
| 48 | + boxes = [] |
| 49 | + scaled_boxes = [] |
| 50 | + center_points = [] |
| 51 | + y = adapted_miny |
| 52 | + while y <= adapted_maxy: |
| 53 | + x = adapted_minx |
| 54 | + while x <= adapted_maxx: |
| 55 | + box = translate(square, x, y) |
| 56 | + if shape.contains(box): |
| 57 | + boxes, scaled_boxes, center_points, crosses_lr, crosses_rl = add_cross( |
| 58 | + box, scaled_boxes, center_points, crosses_lr, crosses_rl, boxes |
| 59 | + ) |
| 60 | + elif shape.intersects(box): |
| 61 | + intersection = box.intersection(shape) |
| 62 | + intersection_area = intersection.area |
| 63 | + if intersection_area / full_square_area * 100 > fill.cross_coverage: |
| 64 | + boxes, scaled_boxes, center_points, crosses_lr, crosses_rl = add_cross( |
| 65 | + box, scaled_boxes, center_points, crosses_lr, crosses_rl, boxes |
| 66 | + ) |
| 67 | + x += square_size |
| 68 | + y += square_size |
| 69 | + check_stop_flag() |
| 70 | + |
| 71 | + if not crosses_lr: |
| 72 | + return [] |
| 73 | + |
| 74 | + lr = ensure_multi_line_string(line_merge(MultiLineString(crosses_lr))) |
| 75 | + rl = ensure_multi_line_string(line_merge(MultiLineString(crosses_rl))) |
| 76 | + |
| 77 | + clamp = False |
| 78 | + outline = unary_union(boxes) |
| 79 | + if outline.geom_type == 'MultiPolygon': |
| 80 | + outline = unary_union(scaled_boxes) |
| 81 | + if outline.geom_type == 'MultiPolygon': |
| 82 | + # we will have to run this on multiple outline shapes |
| 83 | + return cross_stitch_multiple(outline, fill, starting_point, ending_point) |
| 84 | + clamp = True |
| 85 | + |
| 86 | + # used for snapping |
| 87 | + center_points = MultiPoint(center_points) |
| 88 | + |
| 89 | + nodes = get_line_endpoints(rl) |
| 90 | + nodes.extend(get_line_endpoints(lr)) |
| 91 | + |
| 92 | + check_stop_flag() |
| 93 | + |
| 94 | + starting_point, ending_point = get_start_and_end(fill.max_stitch_length, starting_point, ending_point, lr, rl) |
| 95 | + |
| 96 | + stitches = _lines_to_stitches( |
| 97 | + lr, crosses_rl, outline, stitch_length, fill.bean_stitch_repeats, |
| 98 | + starting_point, ending_point, nodes, center_points, clamp |
| 99 | + ) |
| 100 | + starting_point = InkstitchPoint(*stitches[-1]) |
| 101 | + stitches.extend( |
| 102 | + _lines_to_stitches( |
| 103 | + rl, crosses_rl, outline, stitch_length, fill.bean_stitch_repeats, |
| 104 | + starting_point, ending_point, nodes, center_points, clamp |
| 105 | + ) |
| 106 | + ) |
| 107 | + |
| 108 | + return stitches |
| 109 | + |
| 110 | + |
| 111 | +def cross_stitch_multiple(outline, fill, starting_point, ending_point): |
| 112 | + shapes = list(outline.geoms) |
| 113 | + if starting_point: |
| 114 | + shapes.sort(key=lambda shape: shape.distance(Point(starting_point))) |
| 115 | + else: |
| 116 | + shapes.sort(key=lambda shape: shape.bounds[0]) |
| 117 | + stitches = [] |
| 118 | + for i, polygon in enumerate(shapes): |
| 119 | + if i < len(shapes) - 1: |
| 120 | + end = nearest_points(polygon, shapes[i+1])[0].coords |
| 121 | + else: |
| 122 | + end = ending_point |
| 123 | + stitches.extend(cross_stitch(fill, polygon, starting_point, end)) |
| 124 | + starting_point = InkstitchPoint(*stitches[-1]) |
| 125 | + return stitches |
| 126 | + |
| 127 | + |
| 128 | +def get_start_and_end(stitch_length, starting_point, ending_point, lr, rl): |
| 129 | + if starting_point is not None: |
| 130 | + starting_point = snap(Point(starting_point), lr, tolerance=stitch_length).coords |
| 131 | + if ending_point is not None: |
| 132 | + ending_point = snap(Point(ending_point), rl, tolerance=stitch_length).coords |
| 133 | + return starting_point, ending_point |
| 134 | + |
| 135 | + |
| 136 | +def get_line_endpoints(multilinestring): |
| 137 | + nodes = [] |
| 138 | + for line in multilinestring.geoms: |
| 139 | + coords = list(line.coords) |
| 140 | + nodes.extend((coords[0], coords[-1])) |
| 141 | + return nodes |
| 142 | + |
| 143 | + |
| 144 | +def add_cross(box, scaled_boxes, center_points, crosses_lr, crosses_rl, boxes): |
| 145 | + minx, miny, maxx, maxy = box.bounds |
| 146 | + center_points.append(box.centroid) |
| 147 | + crosses_lr.append(LineString([(minx, miny), (maxx, maxy)])) |
| 148 | + crosses_rl.append(LineString([(maxx, miny), (minx, maxy)])) |
| 149 | + boxes.append(box) |
| 150 | + # scaling the outline allows us to connect otherwise unconnected boxes |
| 151 | + box = scale(box, xfact=1.000000000000001, yfact=1.000000000000001) |
| 152 | + scaled_boxes.append(box) |
| 153 | + return boxes, scaled_boxes, center_points, crosses_lr, crosses_rl |
| 154 | + |
| 155 | + |
| 156 | +def _lines_to_stitches( |
| 157 | + line_geoms, travel_edges, shape, stitch_length, |
| 158 | + bean_stitch_repeats, starting_point, ending_point, nodes, center_points, clamp): |
| 159 | + segments = [] |
| 160 | + for line in line_geoms.geoms: |
| 161 | + segments.append(list(line.coords)) |
| 162 | + |
| 163 | + fill_stitch_graph = build_fill_stitch_graph(shape, segments, starting_point, ending_point) |
| 164 | + graph_make_valid(fill_stitch_graph) |
| 165 | + |
| 166 | + if networkx.is_empty(fill_stitch_graph): |
| 167 | + return fallback(shape, stitch_length, 0.2) |
| 168 | + if not networkx.is_connected(fill_stitch_graph): |
| 169 | + return fallback(shape, stitch_length, 0.2) |
| 170 | + else: |
| 171 | + graph_make_valid(fill_stitch_graph) |
| 172 | + |
| 173 | + travel_graph = build_travel_graph(fill_stitch_graph, shape, travel_edges, nodes) |
| 174 | + graph_make_valid(travel_graph) |
| 175 | + path = find_stitch_path(fill_stitch_graph, travel_graph, starting_point, ending_point, False) |
| 176 | + result = path_to_stitches( |
| 177 | + shape, path, travel_graph, fill_stitch_graph, stitch_length, center_points, clamp |
| 178 | + ) |
| 179 | + result = collapse_travel_edges(result) |
| 180 | + result = _apply_bean_stitch_and_repeats(result, 1, bean_stitch_repeats) |
| 181 | + return result |
| 182 | + |
| 183 | + |
| 184 | +def collapse_travel_edges(result): |
| 185 | + new_sitches = [] |
| 186 | + last_travel_stitches = [] |
| 187 | + for i, stitch in enumerate(result): |
| 188 | + if 'auto_fill_travel' in stitch.tags: |
| 189 | + if stitch in last_travel_stitches: |
| 190 | + last_travel_stitches = last_travel_stitches[0:last_travel_stitches.index(stitch)] |
| 191 | + last_travel_stitches.append(stitch) |
| 192 | + else: |
| 193 | + last_travel_stitches.append(stitch) |
| 194 | + new_sitches.extend(last_travel_stitches) |
| 195 | + last_travel_stitches = [] |
| 196 | + if last_travel_stitches: |
| 197 | + new_sitches.extend(last_travel_stitches) |
| 198 | + return new_sitches |
| 199 | + |
| 200 | + |
| 201 | +def path_to_stitches(shape, path, travel_graph, fill_stitch_graph, stitch_length, center_points, clamp): |
| 202 | + # path = collapse_sequential_outline_edges(path, fill_stitch_graph) |
| 203 | + |
| 204 | + stitches = [] |
| 205 | + if not path[0].is_segment(): |
| 206 | + stitches.append(Stitch(*path[0].nodes[0])) |
| 207 | + |
| 208 | + for i, edge in enumerate(path): |
| 209 | + check_stop_flag() |
| 210 | + if edge.is_segment(): |
| 211 | + current_edge = fill_stitch_graph[edge[0]][edge[-1]]['segment'] |
| 212 | + path_geometry = current_edge['geometry'] |
| 213 | + |
| 214 | + if edge[0] != path_geometry.coords[0]: |
| 215 | + path_geometry = reverse_line_string(path_geometry) |
| 216 | + |
| 217 | + stitches.extend([Stitch(*point, tags=["auto_fill", "fill_row"]) for point in path_geometry.coords]) |
| 218 | + |
| 219 | + # note: gap fill segments won't be in the graph |
| 220 | + if fill_stitch_graph.has_edge(edge[0], edge[1], key='segment'): |
| 221 | + travel_graph.remove_edges_from(fill_stitch_graph[edge[0]][edge[1]]['segment'].get('underpath_edges', [])) |
| 222 | + else: |
| 223 | + stitches.extend(travel(shape, travel_graph, edge, center_points, stitch_length, clamp)) |
| 224 | + |
| 225 | + return stitches |
| 226 | + |
| 227 | + |
| 228 | +def travel(shape, travel_graph, edge, center_points, stitch_length, clamp=True): |
| 229 | + """Create stitches to get from one point on an outline of the shape to another.""" |
| 230 | + |
| 231 | + start, end = edge |
| 232 | + try: |
| 233 | + path = networkx.shortest_path(travel_graph, start, end, weight='weight') |
| 234 | + except networkx.NetworkXNoPath: |
| 235 | + # This may not look good, but it prevents the fill from failing (which hopefully never happens) |
| 236 | + path = [start, end] |
| 237 | + |
| 238 | + path = [InkstitchPoint.from_tuple(point) for point in path] |
| 239 | + if not path: |
| 240 | + # This may happen on very small shapes. |
| 241 | + # Simply return nothing as we do not want to error out |
| 242 | + return [] |
| 243 | + |
| 244 | + if len(path) > 1 and clamp: |
| 245 | + path = clamp_path_to_polygon(path, shape) |
| 246 | + |
| 247 | + stitches = [] |
| 248 | + last_point = None |
| 249 | + for point in path: |
| 250 | + check_stop_flag() |
| 251 | + if last_point is None: |
| 252 | + last_point = point |
| 253 | + continue |
| 254 | + if last_point[0] != point[0] and last_point[1] != point[1]: |
| 255 | + pass |
| 256 | + else: |
| 257 | + line = LineString([last_point, point]) |
| 258 | + if line.length < stitch_length / 2 + 2: |
| 259 | + stitches.append(Stitch(point, tags=["auto_fill_travel"])) |
| 260 | + last_point = point |
| 261 | + continue |
| 262 | + center = list(rotate(line, 90).coords) |
| 263 | + point1 = Point(center[0]) |
| 264 | + if point1.within(shape): |
| 265 | + center_point = point1 |
| 266 | + else: |
| 267 | + center_point = Point(center[1]) |
| 268 | + # snap to avoid problems in edge collapsing later on |
| 269 | + center_point = snap(center_point, center_points, tolerance=0.01) |
| 270 | + stitches.append(Stitch(center_point, tags=["auto_fill_travel"])) |
| 271 | + stitches.append(Stitch(*point, tags=["auto_fill_travel"])) |
| 272 | + last_point = point |
| 273 | + |
| 274 | + return stitches |
| 275 | + |
| 276 | + |
| 277 | +def build_travel_graph(fill_stitch_graph, shape, travel_edges, nodes): |
| 278 | + """Build a graph for travel stitches. |
| 279 | + """ |
| 280 | + graph = networkx.MultiGraph() |
| 281 | + |
| 282 | + # Add all the nodes from the main graph. This will be all of the endpoints |
| 283 | + # of the rows of stitches. Every node will be on the outline of the shape. |
| 284 | + # They'll all already have their `outline` and `projection` tags set. |
| 285 | + graph.add_nodes_from(fill_stitch_graph.nodes(data=True)) |
| 286 | + |
| 287 | + # This will ensure that a path traveling inside the shape can reach its |
| 288 | + # target on the outline, which will be one of the points added above. |
| 289 | + tag_nodes_with_outline_and_projection(graph, shape, nodes) |
| 290 | + add_edges_between_outline_nodes(graph, duplicate_every_other=True) |
| 291 | + |
| 292 | + process_travel_edges(graph, fill_stitch_graph, shape, travel_edges) |
| 293 | + |
| 294 | + debug.log_graph(graph, "travel graph") |
| 295 | + |
| 296 | + return graph |
0 commit comments