kopia lustrzana https://github.com/inkstitch/inkstitch
				
				
				
			
		
			
				
	
	
		
			285 wiersze
		
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
	
			
		
		
	
	
			285 wiersze
		
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
	
| from math import atan2, copysign
 | |
| from random import random
 | |
| 
 | |
| import numpy as np
 | |
| import shapely.prepared
 | |
| from networkx import is_empty
 | |
| from shapely import geometry as shgeo
 | |
| from shapely.affinity import translate
 | |
| from shapely.ops import linemerge, nearest_points, unary_union
 | |
| 
 | |
| from ..debug import debug
 | |
| from ..stitch_plan import Stitch
 | |
| from ..utils.geometry import Point as InkstitchPoint
 | |
| from ..utils.geometry import (ensure_geometry_collection,
 | |
|                               ensure_multi_line_string, reverse_line_string)
 | |
| from ..utils.threading import check_stop_flag
 | |
| from .auto_fill import (auto_fill, build_fill_stitch_graph, build_travel_graph,
 | |
|                         collapse_sequential_outline_edges, find_stitch_path,
 | |
|                         graph_make_valid, travel)
 | |
| 
 | |
| 
 | |
| def guided_fill(shape,
 | |
|                 guideline,
 | |
|                 angle,
 | |
|                 row_spacing,
 | |
|                 num_staggers,
 | |
|                 max_stitch_length,
 | |
|                 running_stitch_length,
 | |
|                 running_stitch_tolerance,
 | |
|                 skip_last,
 | |
|                 starting_point,
 | |
|                 ending_point,
 | |
|                 underpath,
 | |
|                 strategy
 | |
|                 ):
 | |
|     segments = intersect_region_with_grating_guideline(shape, guideline, row_spacing, num_staggers, max_stitch_length, strategy)
 | |
|     if not segments:
 | |
|         return fallback(shape, guideline, row_spacing, max_stitch_length, running_stitch_length, running_stitch_tolerance,
 | |
|                         num_staggers, skip_last, starting_point, ending_point, underpath)
 | |
| 
 | |
|     fill_stitch_graph = build_fill_stitch_graph(shape, segments, starting_point, ending_point)
 | |
| 
 | |
|     if is_empty(fill_stitch_graph):
 | |
|         return fallback(shape, guideline, row_spacing, max_stitch_length, running_stitch_length, running_stitch_tolerance,
 | |
|                         num_staggers, skip_last, starting_point, ending_point, underpath)
 | |
|     fill_stitch_graph = graph_make_valid(fill_stitch_graph)
 | |
| 
 | |
|     travel_graph = build_travel_graph(fill_stitch_graph, shape, angle, underpath)
 | |
|     path = find_stitch_path(fill_stitch_graph, travel_graph, starting_point, ending_point)
 | |
|     result = path_to_stitches(shape, path, travel_graph, fill_stitch_graph,
 | |
|                               max_stitch_length, running_stitch_length, running_stitch_tolerance, skip_last,
 | |
|                               underpath)
 | |
| 
 | |
|     return result
 | |
| 
 | |
| 
 | |
| def fallback(shape, guideline, row_spacing, max_stitch_length, running_stitch_length, running_stitch_tolerance,
 | |
|              num_staggers, skip_last, starting_point, ending_point, underpath):
 | |
|     # fall back to normal auto-fill with an angle that matches the guideline (sorta)
 | |
|     guide_start, guide_end = [guideline.coords[0], guideline.coords[-1]]
 | |
|     angle = atan2(guide_end[1] - guide_start[1], guide_end[0] - guide_start[0]) * -1
 | |
|     return auto_fill(shape, angle, row_spacing, None, max_stitch_length, running_stitch_length, running_stitch_tolerance,
 | |
|                      num_staggers, skip_last, starting_point, ending_point, underpath)
 | |
| 
 | |
| 
 | |
| def path_to_stitches(shape, path, travel_graph, fill_stitch_graph,
 | |
|                      stitch_length, running_stitch_length, running_stitch_tolerance, skip_last,
 | |
|                      underpath):
 | |
|     path = collapse_sequential_outline_edges(path, fill_stitch_graph)
 | |
| 
 | |
|     stitches = []
 | |
| 
 | |
|     # If the very first stitch is travel, we'll omit it in travel(), so add it here.
 | |
|     if not path[0].is_segment():
 | |
|         stitches.append(Stitch(*path[0].nodes[0]))
 | |
| 
 | |
|     for edge in path:
 | |
|         check_stop_flag()
 | |
| 
 | |
|         if edge.is_segment():
 | |
|             current_edge = fill_stitch_graph[edge[0]][edge[-1]]['segment']
 | |
|             path_geometry = current_edge['geometry']
 | |
| 
 | |
|             if edge[0] != path_geometry.coords[0]:
 | |
|                 path_geometry = reverse_line_string(path_geometry)
 | |
| 
 | |
|             new_stitches = [Stitch(*point) for point in path_geometry.coords]
 | |
| 
 | |
|             # need to tag stitches
 | |
| 
 | |
|             if skip_last:
 | |
|                 del new_stitches[-1]
 | |
| 
 | |
|             stitches.extend(new_stitches)
 | |
| 
 | |
|             travel_graph.remove_edges_from(fill_stitch_graph[edge[0]][edge[1]]['segment'].get('underpath_edges', []))
 | |
|         else:
 | |
|             stitches.extend(travel(shape, travel_graph, edge, running_stitch_length, running_stitch_tolerance, skip_last, underpath))
 | |
| 
 | |
|     return stitches
 | |
| 
 | |
| 
 | |
| def extend_line(line, shape):
 | |
|     (minx, miny, maxx, maxy) = shape.bounds
 | |
| 
 | |
|     upper_left = InkstitchPoint(minx, miny)
 | |
|     lower_right = InkstitchPoint(maxx, maxy)
 | |
|     length = (upper_left - lower_right).length()
 | |
| 
 | |
|     start_point = InkstitchPoint.from_tuple(line.coords[0])
 | |
|     end_point = InkstitchPoint.from_tuple(line.coords[-1])
 | |
|     direction = (end_point - start_point).unit()
 | |
| 
 | |
|     new_start_point = start_point - direction * length
 | |
|     new_end_point = end_point + direction * length
 | |
| 
 | |
|     # without this, we seem especially likely to run into this libgeos bug:
 | |
|     #   https://github.com/shapely/shapely/issues/820
 | |
|     new_start_point += InkstitchPoint(random() * 0.01, random() * 0.01)
 | |
|     new_end_point += InkstitchPoint(random() * 0.01, random() * 0.01)
 | |
| 
 | |
|     return shgeo.LineString((new_start_point, *line.coords, new_end_point))
 | |
| 
 | |
| 
 | |
| def repair_multiple_parallel_offset_curves(multi_line):
 | |
|     lines = ensure_multi_line_string(linemerge(multi_line))
 | |
|     longest_line = max(lines.geoms, key=lambda line: line.length)
 | |
| 
 | |
|     # need simplify to avoid doubled points caused by linemerge
 | |
|     return longest_line.simplify(0.01, False)
 | |
| 
 | |
| 
 | |
| def repair_non_simple_line(line):
 | |
|     repaired = unary_union(line)
 | |
|     counter = 0
 | |
|     # Do several iterations since we might have several concatenated selfcrossings
 | |
|     while repaired.geom_type != 'LineString' and counter < 4:
 | |
|         line_segments = []
 | |
|         for line_seg in repaired.geoms:
 | |
|             if not line_seg.is_ring:
 | |
|                 line_segments.append(line_seg)
 | |
| 
 | |
|         repaired = unary_union(linemerge(line_segments))
 | |
|         counter += 1
 | |
|     if repaired.geom_type != 'LineString':
 | |
|         # They gave us a line with complicated self-intersections.  Use a fallback.
 | |
|         return shgeo.LineString((line.coords[0], line.coords[-1]))
 | |
|     else:
 | |
|         return repaired
 | |
| 
 | |
| 
 | |
| def take_only_line_strings(thing):
 | |
|     things = ensure_geometry_collection(thing)
 | |
|     line_strings = [line for line in things.geoms if isinstance(line, shgeo.LineString)]
 | |
| 
 | |
|     return shgeo.MultiLineString(line_strings)
 | |
| 
 | |
| 
 | |
| def apply_stitches(line, max_stitch_length, num_staggers, row_spacing, row_num, threshold=None):
 | |
|     if num_staggers == 0:
 | |
|         num_staggers = 1  # sanity check to avoid division by zero.
 | |
|     start = ((row_num / num_staggers) % 1) * max_stitch_length
 | |
|     projections = np.arange(start, line.length, max_stitch_length)
 | |
|     points = np.array([line.interpolate(projection).coords[0] for projection in projections])
 | |
| 
 | |
|     if len(points) <= 2:
 | |
|         return line
 | |
| 
 | |
|     stitched_line = shgeo.LineString(points)
 | |
| 
 | |
|     # stitched_line may round corners, which will look terrible.  This finds the
 | |
|     # corners.
 | |
|     if not threshold:
 | |
|         threshold = row_spacing / 2.0
 | |
|     simplified_line = line.simplify(threshold, False)
 | |
|     simplified_points = [shgeo.Point(x, y) for x, y in simplified_line.coords]
 | |
| 
 | |
|     extra_points = []
 | |
|     extra_point_projections = []
 | |
|     for point in simplified_points:
 | |
|         if point.distance(stitched_line) > threshold:
 | |
|             extra_points.append(point.coords[0])
 | |
|             extra_point_projections.append(line.project(point))
 | |
| 
 | |
|     # Now we need to insert the new points into their correct spots in the line.
 | |
|     indices = np.searchsorted(projections, extra_point_projections)
 | |
|     if len(indices) > 0:
 | |
|         points = np.insert(points, indices, extra_points, axis=0)
 | |
| 
 | |
|     return shgeo.LineString(points)
 | |
| 
 | |
| 
 | |
| def prepare_guide_line(line, shape):
 | |
|     if line.geom_type != 'LineString' or not line.is_simple:
 | |
|         line = repair_non_simple_line(line)
 | |
| 
 | |
|     if line.is_ring:
 | |
|         # If they pass us a ring, break it to avoid dividing by zero when
 | |
|         # calculating a unit vector from start to end.
 | |
|         line = shgeo.LineString(line.coords[:-2])
 | |
| 
 | |
|     # extend the end points away from each other
 | |
|     line = extend_line(line, shape)
 | |
| 
 | |
|     return line
 | |
| 
 | |
| 
 | |
| def clean_offset_line(offset_line):
 | |
|     offset_line = take_only_line_strings(offset_line)
 | |
| 
 | |
|     if isinstance(offset_line, shgeo.MultiLineString):
 | |
|         offset_line = repair_multiple_parallel_offset_curves(offset_line)
 | |
| 
 | |
|     if not offset_line.is_simple:
 | |
|         offset_line = repair_non_simple_line(offset_line)
 | |
| 
 | |
|     return offset_line
 | |
| 
 | |
| 
 | |
| def _get_start_row(line, shape, row_spacing, line_direction):
 | |
|     if line.intersects(shape):
 | |
|         return 0
 | |
| 
 | |
|     point1, point2 = nearest_points(line, shape.centroid)
 | |
|     distance = point1.distance(point2)
 | |
|     row = int(distance / row_spacing)
 | |
| 
 | |
|     # This flips the sign of the starting row if the shape is on the other side
 | |
|     # of the guide line
 | |
|     shape_direction = InkstitchPoint.from_shapely_point(point2) - InkstitchPoint.from_shapely_point(point1)
 | |
|     return copysign(row, shape_direction * line_direction)
 | |
| 
 | |
| 
 | |
| def intersect_region_with_grating_guideline(shape, line, row_spacing, num_staggers, max_stitch_length, strategy):
 | |
|     line = prepare_guide_line(line, shape)
 | |
| 
 | |
|     debug.log_line_string(shape.exterior, "guided fill shape")
 | |
| 
 | |
|     translate_direction = InkstitchPoint(*line.coords[-1]) - InkstitchPoint(*line.coords[0])
 | |
|     translate_direction = translate_direction.unit().rotate_left()
 | |
| 
 | |
|     shape_envelope = shapely.prepared.prep(shape.convex_hull)
 | |
| 
 | |
|     start_row = _get_start_row(line, shape, row_spacing, translate_direction)
 | |
|     row = start_row
 | |
|     direction = 1
 | |
|     offset_line = None
 | |
|     rows = []
 | |
|     while True:
 | |
|         check_stop_flag()
 | |
| 
 | |
|         if strategy == 0:
 | |
|             translate_amount = translate_direction * row * row_spacing
 | |
|             offset_line = translate(line, xoff=translate_amount.x, yoff=translate_amount.y)
 | |
|         elif strategy == 1:
 | |
|             offset = row * row_spacing
 | |
|             if offset == 0:
 | |
|                 # this is needed for macOS builds
 | |
|                 offset = 0.0001
 | |
|             offset_line = line.parallel_offset(offset, 'left', join_style=shgeo.JOIN_STYLE.round)
 | |
| 
 | |
|         offset_line = clean_offset_line(offset_line)
 | |
| 
 | |
|         if strategy == 1 and direction == -1:
 | |
|             # negative parallel offsets are reversed, so we need to compensate
 | |
|             offset_line = reverse_line_string(offset_line)
 | |
| 
 | |
|         debug.log_line_string(offset_line, f"offset {row}")
 | |
| 
 | |
|         stitched_line = apply_stitches(offset_line, max_stitch_length, num_staggers, row_spacing, row)
 | |
|         intersection = shape.intersection(stitched_line)
 | |
| 
 | |
|         if not intersection.is_empty and shape_envelope.intersects(stitched_line):
 | |
|             for segment in take_only_line_strings(intersection).geoms:
 | |
|                 rows.append(segment.coords[:])
 | |
|             row += direction
 | |
|         else:
 | |
|             if direction == 1:
 | |
|                 direction = -1
 | |
|                 row = start_row - 1
 | |
|             else:
 | |
|                 break
 | |
| 
 | |
|     return rows
 |