22from math import atan2 , ceil
33
44import numpy as np
5+ from networkx import connected_components , is_empty
56from shapely import prepare
67from shapely .affinity import rotate , scale , translate
78from shapely .geometry import LineString , Point , Polygon
89from shapely .ops import substring
910
1011from ..elements import SatinColumn
12+ from ..stitch_plan import Stitch
1113from ..utils import Point as InkstitchPoint
1214from ..utils import prng
13- from ..utils .geometry import line_string_to_point_list
15+ from ..utils .geometry import (ensure_multi_line_string ,
16+ line_string_to_point_list , reverse_line_string )
1417from ..utils .threading import check_stop_flag
18+ from .auto_fill import (build_fill_stitch_graph , build_travel_graph ,
19+ collapse_sequential_outline_edges , find_stitch_path ,
20+ graph_make_valid , travel )
1521from .guided_fill import apply_stitches
1622from .running_stitch import even_running_stitch , running_stitch
1723
@@ -26,10 +32,11 @@ def ripple_stitch(stroke):
2632 If more sublines are present interpolation will take place between the first two.
2733 '''
2834
29- if stroke .as_multi_line_string ().length < 0.1 :
35+ if stroke .as_multi_line_string (False ).length < 0.1 :
3036 return []
3137
3238 is_linear , helper_lines , grid_lines = _get_helper_lines (stroke )
39+
3340 if not helper_lines :
3441 return []
3542
@@ -43,12 +50,169 @@ def ripple_stitch(stroke):
4350 if stroke .reverse :
4451 stitches .reverse ()
4552
46- if stitches and stroke .grid_size != 0 :
47- stitches .extend (_do_grid (stroke , grid_lines , skip_start , skip_end , is_linear , stitches [- 1 ]))
48- if stroke .grid_first :
49- stitches = stitches [::- 1 ]
53+ remove_end_travel = True
54+ if stroke .grid_size != 0 :
55+ remove_end_travel = False
56+ stitch_groups = _route_clipped_stitches (stroke , stitches , True , remove_end_travel )
57+
58+ if stroke .repeats > 1 :
59+ repeated_groups = []
60+ for stitch_group in stitch_groups :
61+ repeated_groups .append (_repeat_coords (stitch_group , stroke .repeats ))
62+ stitch_groups = repeated_groups
63+
64+ if stitch_groups and stroke .grid_size != 0 :
65+ grid_sitches = _do_grid (stroke , grid_lines , skip_start , skip_end , is_linear , stitches [- 1 ])
66+ stitch_groups .extend (_route_clipped_stitches (stroke , grid_sitches , False , True , InkstitchPoint (* stitch_groups [- 1 ][- 1 ])))
67+
68+ if stroke .grid_size != 0 and stroke .grid_first :
69+ reversed_groups = []
70+ for stitch_group in stitch_groups :
71+ reversed_groups .append (stitch_group [::- 1 ])
72+ stitch_groups = reversed_groups [::- 1 ]
73+
74+ return stitch_groups
75+
76+
77+ def _route_clipped_stitches (stroke , stitches , remove_start_travel = True , remove_end_travel = True , starting_point = None ):
78+ if stroke .clip_shape is None :
79+ return [stitches ]
80+
81+ clip = stroke .clip_shape
82+ line_strings = LineString (stitches )
83+ clipped_strings = line_strings .intersection (clip )
84+ clipped_strings = ensure_multi_line_string (clipped_strings )
85+ if not clipped_strings :
86+ return [stitches ]
87+
88+ segments = _prepare_line_segments (clip , clipped_strings )
89+
90+ if not starting_point :
91+ starting_point = InkstitchPoint (* segments [0 ][0 ])
92+ ending_point = InkstitchPoint (* segments [- 1 ][- 1 ])
93+ routed_segments = _auto_route_segments (clip , stroke , segments , starting_point , ending_point , stitches )
94+
95+ if not routed_segments :
96+ return [stitches ]
97+
98+ # multi-part stitch result (clip cuts off one or more parts of the original path)
99+ # TODO: sort multipart ripples with grid
100+ _sort_segments (routed_segments , starting_point , ending_point )
101+ stitches = _segments_to_stitches (segments , routed_segments , starting_point , ending_point , remove_start_travel , remove_end_travel )
102+
103+ return stitches
104+
105+
106+ def _segments_to_stitches (segments , routed_segments , starting_point , ending_point , remove_start_travel , remove_end_travel ):
107+ stitches = []
108+ for segment in routed_segments :
109+ current_segment = segment
110+ if remove_start_travel :
111+ for i , stitch in enumerate (current_segment ):
112+ if 'auto_fill_travel' not in stitch .tags :
113+ current_segment = current_segment [i :]
114+ break
115+ if remove_end_travel :
116+ current_segment .reverse ()
117+ for i , stitch in enumerate (current_segment ):
118+ if 'auto_fill_travel' not in stitch .tags :
119+ current_segment = current_segment [i :]
120+ break
121+ current_segment .reverse ()
122+
123+ if current_segment :
124+ stitches .append (current_segment )
125+ current_segment = []
126+ return stitches
127+
128+
129+ def _sort_segments (routed_segments , starting_point , ending_point ):
130+ if len (routed_segments ) == 1 :
131+ return
132+ start_segment_index = [i for i , segment in enumerate (routed_segments ) if segment [0 ] == starting_point ]
133+ if start_segment_index :
134+ routed_segments .insert (0 , routed_segments .pop (start_segment_index [0 ]))
135+ end_segment_index = [(i , segment ) for i , segment in enumerate (routed_segments ) if segment [- 1 ] == ending_point ]
136+ if end_segment_index and end_segment_index != 0 :
137+ routed_segments .pop (end_segment_index [0 ][0 ])
138+ routed_segments .append (end_segment_index [0 ][1 ])
139+
140+
141+ def _auto_route_segments (clip , stroke , segments , starting_point , ending_point , stitches ):
50142
51- return _repeat_coords (stitches , stroke .repeats )
143+ fill_stitch_graph = build_fill_stitch_graph (clip , segments , starting_point , ending_point )
144+
145+ if is_empty (fill_stitch_graph ):
146+ return [stitches ]
147+
148+ # clipping may split the stitch path into several unconnected sections
149+ connected_graphs = [fill_stitch_graph .subgraph (c ).copy () for c in connected_components (fill_stitch_graph )]
150+ result = []
151+ for ripple_graph in connected_graphs :
152+ graph_make_valid (ripple_graph )
153+
154+ travel_graph = build_travel_graph (ripple_graph , clip , 0 , False )
155+ path = find_stitch_path (ripple_graph , travel_graph , starting_point , ending_point , True )
156+ stitches = path_to_stitches (
157+ clip , segments , path , travel_graph , ripple_graph ,
158+ stroke .running_stitch_length , stroke .running_stitch_tolerance , False , False )
159+ result .append (stitches )
160+ return result
161+
162+
163+ def _prepare_line_segments (clip , clipped_strings ):
164+ # merge continuous lines (they may have been split up at self-intersections).
165+ segments = []
166+ current_segment = []
167+ for line in clipped_strings .geoms :
168+ coords = line .coords
169+ first_point = Point (coords [0 ])
170+ if first_point .distance (clip .boundary ) < 0.0001 and current_segment :
171+ segments .append (current_segment )
172+ current_segment = []
173+
174+ # remove additional stitches which were a byproduct of splitting linestrings at intersections
175+ if len (current_segment ) > 1 :
176+ current_segment = current_segment [:- 1 ]
177+ coords = coords [1 :]
178+
179+ current_segment .extend ([(coord [0 ], coord [1 ]) for coord in coords ])
180+ if current_segment :
181+ segments .append (current_segment )
182+
183+ return segments
184+
185+
186+ def path_to_stitches (shape , segments , path , travel_graph , fill_stitch_graph , running_stitch_length , running_stitch_tolerance , skip_last , underpath ):
187+ path = collapse_sequential_outline_edges (path , fill_stitch_graph )
188+
189+ stitches = []
190+
191+ # If the very first stitch is travel, we'll omit it in travel(), so add it here.
192+ if not path [0 ].is_segment ():
193+ stitches .append (Stitch (* path [0 ].nodes [0 ], tags = {'auto_fill_travel' }))
194+
195+ for edge in path :
196+ if edge .is_segment ():
197+ current_edge = fill_stitch_graph [edge [0 ]][edge [- 1 ]]['segment' ]
198+ path_geometry = current_edge ['geometry' ]
199+
200+ if edge [0 ] != path_geometry .coords [0 ]:
201+ path_geometry = reverse_line_string (path_geometry )
202+
203+ new_stitches = [Stitch (* point ) for point in path_geometry .coords ]
204+
205+ # need to tag stitches
206+ if skip_last :
207+ del new_stitches [- 1 ]
208+
209+ stitches .extend (new_stitches )
210+
211+ travel_graph .remove_edges_from (fill_stitch_graph [edge [0 ]][edge [1 ]]['segment' ].get ('underpath_edges' , []))
212+ else :
213+ stitches .extend (travel (shape , travel_graph , edge , running_stitch_length , running_stitch_tolerance , skip_last , underpath , False ))
214+
215+ return stitches
52216
53217
54218def _get_stitches (stroke , is_linear , lines , skip_start ):
@@ -197,14 +361,14 @@ def _line_count_adjust(stroke, num_lines):
197361 num_lines -= 1
198362 # ensure minimum line count
199363 num_lines = max (1 , num_lines )
200- if stroke .is_closed or stroke .join_style == 1 :
364+ if stroke .is_closed_unclipped or stroke .join_style == 1 :
201365 # for flat join styles we need to add an other line
202366 num_lines += 1
203367 return num_lines
204368
205369
206370def _get_helper_lines (stroke ):
207- lines = stroke .as_multi_line_string ().geoms
371+ lines = stroke .as_multi_line_string (False ).geoms
208372 if len (lines ) > 1 :
209373 helper_lines = _get_satin_ripple_helper_lines (stroke )
210374 if stroke .grid_size > 0 :
@@ -228,7 +392,7 @@ def _get_helper_lines(stroke):
228392 stroke .running_stitch_tolerance )
229393 )
230394
231- if stroke .is_closed :
395+ if stroke .is_closed_unclipped :
232396 helper_lines = _get_circular_ripple_helper_lines (stroke , outline )
233397 return False , helper_lines , helper_lines
234398 elif stroke .join_style == 1 :
0 commit comments