kopia lustrzana https://github.com/inkstitch/inkstitch
				
				
				
			
		
			
				
	
	
		
			191 wiersze
		
	
	
		
			7.5 KiB
		
	
	
	
		
			Python
		
	
	
			
		
		
	
	
			191 wiersze
		
	
	
		
			7.5 KiB
		
	
	
	
		
			Python
		
	
	
| from shapely import geometry as shgeo
 | |
| from shapely.ops import linemerge, unary_union
 | |
| 
 | |
| from .auto_fill import (build_fill_stitch_graph,
 | |
|                         build_travel_graph, collapse_sequential_outline_edges, fallback,
 | |
|                         find_stitch_path, graph_is_valid, travel)
 | |
| from .running_stitch import running_stitch
 | |
| from ..debug import debug
 | |
| from ..i18n import _
 | |
| from ..stitch_plan import Stitch
 | |
| from ..utils.geometry import Point as InkstitchPoint, reverse_line_string
 | |
| 
 | |
| 
 | |
| @debug.time
 | |
| def guided_fill(shape,
 | |
|                 guideline,
 | |
|                 angle,
 | |
|                 row_spacing,
 | |
|                 max_stitch_length,
 | |
|                 min_stitch_length,
 | |
|                 running_stitch_length,
 | |
|                 skip_last,
 | |
|                 starting_point,
 | |
|                 ending_point=None,
 | |
|                 underpath=True,
 | |
|                 offset_by_half=True):
 | |
|     try:
 | |
|         segments = intersect_region_with_grating_guideline(shape, guideline, row_spacing)
 | |
|         fill_stitch_graph = build_fill_stitch_graph(shape, segments, starting_point, ending_point)
 | |
|     except ValueError:
 | |
|         # Small shapes will cause the graph to fail - min() arg is an empty sequence through insert node
 | |
|         return fallback(shape, running_stitch_length)
 | |
| 
 | |
|     if not graph_is_valid(fill_stitch_graph, shape, max_stitch_length):
 | |
|         return fallback(shape, running_stitch_length)
 | |
| 
 | |
|     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(path, travel_graph, fill_stitch_graph, angle, row_spacing,
 | |
|                               max_stitch_length, min_stitch_length, running_stitch_length, skip_last, offset_by_half)
 | |
| 
 | |
|     return result
 | |
| 
 | |
| 
 | |
| @debug.time
 | |
| def path_to_stitches(path, travel_graph, fill_stitch_graph, angle, row_spacing, max_stitch_length, min_stitch_length,
 | |
|                      running_stitch_length, skip_last, offset_by_half):
 | |
|     path = collapse_sequential_outline_edges(path)
 | |
| 
 | |
|     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:
 | |
|         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)
 | |
| 
 | |
|             point_list = [Stitch(*point) for point in path_geometry.coords]
 | |
|             new_stitches = running_stitch(point_list, max_stitch_length)
 | |
| 
 | |
|             # 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(travel_graph, edge[0], edge[1], running_stitch_length, skip_last))
 | |
| 
 | |
|     return stitches
 | |
| 
 | |
| 
 | |
| def extend_line(line, minx, maxx, miny, maxy):
 | |
|     line = line.simplify(0.01, False)
 | |
| 
 | |
|     upper_left = InkstitchPoint(minx, miny)
 | |
|     lower_right = InkstitchPoint(maxx, maxy)
 | |
|     length = (upper_left - lower_right).length()
 | |
| 
 | |
|     point1 = InkstitchPoint(*line.coords[0])
 | |
|     point2 = InkstitchPoint(*line.coords[1])
 | |
|     new_starting_point = point1 - (point2 - point1).unit() * length
 | |
| 
 | |
|     point3 = InkstitchPoint(*line.coords[-2])
 | |
|     point4 = InkstitchPoint(*line.coords[-1])
 | |
|     new_ending_point = point4 + (point4 - point3).unit() * length
 | |
| 
 | |
|     return shgeo.LineString([new_starting_point.as_tuple()] +
 | |
|                             line.coords[1:-1] + [new_ending_point.as_tuple()])
 | |
| 
 | |
| 
 | |
| def repair_multiple_parallel_offset_curves(multi_line):
 | |
|     lines = linemerge(multi_line)
 | |
|     lines = list(lines.geoms)
 | |
|     max_length = -1
 | |
|     max_length_idx = -1
 | |
|     for idx, subline in enumerate(lines):
 | |
|         if subline.length > max_length:
 | |
|             max_length = subline.length
 | |
|             max_length_idx = idx
 | |
|     # need simplify to avoid doubled points caused by linemerge
 | |
|     return lines[max_length_idx].simplify(0.01, False)
 | |
| 
 | |
| 
 | |
| def repair_non_simple_lines(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':
 | |
|         raise ValueError(
 | |
|             _("Guide line (or offsetted instance) is self crossing!"))
 | |
|     else:
 | |
|         return repaired
 | |
| 
 | |
| 
 | |
| def intersect_region_with_grating_guideline(shape, line, row_spacing, flip=False):  # noqa: C901
 | |
| 
 | |
|     row_spacing = abs(row_spacing)
 | |
|     (minx, miny, maxx, maxy) = shape.bounds
 | |
|     upper_left = InkstitchPoint(minx, miny)
 | |
|     rows = []
 | |
| 
 | |
|     if line.geom_type != 'LineString' or not line.is_simple:
 | |
|         line = repair_non_simple_lines(line)
 | |
|     # extend the line towards the ends to increase probability that all offsetted curves cross the shape
 | |
|     line = extend_line(line, minx, maxx, miny, maxy)
 | |
| 
 | |
|     line_offsetted = line
 | |
|     res = line_offsetted.intersection(shape)
 | |
|     while isinstance(res, (shgeo.GeometryCollection, shgeo.MultiLineString)) or (not res.is_empty and len(res.coords) > 1):
 | |
|         if isinstance(res, (shgeo.GeometryCollection, shgeo.MultiLineString)):
 | |
|             runs = [line_string.coords for line_string in res.geoms if (
 | |
|                     not line_string.is_empty and len(line_string.coords) > 1)]
 | |
|         else:
 | |
|             runs = [res.coords]
 | |
| 
 | |
|         runs.sort(key=lambda seg: (
 | |
|                 InkstitchPoint(*seg[0]) - upper_left).length())
 | |
|         if flip:
 | |
|             runs.reverse()
 | |
|             runs = [tuple(reversed(run)) for run in runs]
 | |
| 
 | |
|         if row_spacing > 0:
 | |
|             rows.append(runs)
 | |
|         else:
 | |
|             rows.insert(0, runs)
 | |
| 
 | |
|         line_offsetted = line_offsetted.parallel_offset(row_spacing, 'left', 5)
 | |
|         if line_offsetted.geom_type == 'MultiLineString':  # if we got multiple lines take the longest
 | |
|             line_offsetted = repair_multiple_parallel_offset_curves(line_offsetted)
 | |
|         if not line_offsetted.is_simple:
 | |
|             line_offsetted = repair_non_simple_lines(line_offsetted)
 | |
| 
 | |
|         if row_spacing < 0:
 | |
|             line_offsetted = reverse_line_string(line_offsetted)
 | |
|         line_offsetted = line_offsetted.simplify(0.01, False)
 | |
|         res = line_offsetted.intersection(shape)
 | |
|         if row_spacing > 0 and not isinstance(res, (shgeo.GeometryCollection, shgeo.MultiLineString)):
 | |
|             if (res.is_empty or len(res.coords) == 1):
 | |
|                 row_spacing = -row_spacing
 | |
| 
 | |
|                 line_offsetted = line.parallel_offset(row_spacing, 'left', 5)
 | |
|                 if line_offsetted.geom_type == 'MultiLineString':  # if we got multiple lines take the longest
 | |
|                     line_offsetted = repair_multiple_parallel_offset_curves(
 | |
|                         line_offsetted)
 | |
|                 if not line_offsetted.is_simple:
 | |
|                     line_offsetted = repair_non_simple_lines(line_offsetted)
 | |
|                 # using negative row spacing leads as a side effect to reversed offsetted lines - here we undo this
 | |
|                 line_offsetted = reverse_line_string(line_offsetted)
 | |
|                 line_offsetted = line_offsetted.simplify(0.01, False)
 | |
|                 res = line_offsetted.intersection(shape)
 | |
| 
 | |
|     for row in rows:
 | |
|         yield from row
 |