Skip to content

Commit 383f164

Browse files
authored
Fill to satin: process rungs within the fill shape better (#4025)
* fill to satin: process rungs within the fill shape better * fix one rail problem * adjust closed paths starting point
1 parent 15d2950 commit 383f164

File tree

1 file changed

+108
-11
lines changed

1 file changed

+108
-11
lines changed

lib/extensions/fill_to_satin.py

Lines changed: 108 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@
66
from collections import defaultdict
77

88
from inkex import Boolean, Group, Path, PathElement
9-
from shapely.geometry import LineString, MultiLineString, Point
9+
from shapely.geometry import LineString, MultiLineString, MultiPoint, Point
1010
from shapely.ops import linemerge, snap, split, substring
1111

1212
from ..elements import FillStitch, Stroke
1313
from ..gui.abort_message import AbortMessageApp
1414
from ..i18n import _
1515
from ..svg import get_correction_transform
16-
from ..utils import ensure_multi_line_string
16+
from ..utils import ensure_multi_line_string, roll_linear_ring
1717
from .base import InkstitchExtension
1818

1919

@@ -222,18 +222,101 @@ def _combined_segments_to_satin_geoms(self, combined_rails, combined_rungs, sati
222222
for segment_index in set(segments):
223223
segment_geoms.extend(list(satin_segments[segment_index].geoms))
224224
satin_rails = ensure_multi_line_string(linemerge(segment_geoms))
225-
if len(satin_rails.geoms) != 2:
225+
if len(satin_rails.geoms) == 1:
226+
satin_rails = self._fix_single_rail_issue(satin_rails, segments, satin_segments, set(combined_rungs[i]))
227+
if satin_rails is None:
228+
continue
229+
elif len(satin_rails.geoms) != 2:
226230
continue
231+
232+
# adjust rail direction and starting points
227233
satin_rails = [self._adjust_rail_direction(satin_rails)]
234+
satin_rails = [self._adjust_closed_path_starting_point(satin_rails, self.rungs[combined_rungs[i][0]])]
235+
228236
segment_geoms = []
229237
for rung_index in set(combined_rungs[i]):
230238
rung = self.rungs[rung_index]
231239
# satin behaves bad if a rung is positioned directly at the beginning/end section
232-
if rung.distance(Point(satin_rails[0].geoms[0].coords[0])) > 1:
240+
start = Point(satin_rails[0].geoms[0].coords[0])
241+
end = Point(satin_rails[0].geoms[1].coords[-1])
242+
if rung.distance(start) > 1 and rung.distance(end) > 1:
233243
segment_geoms.append(ensure_multi_line_string(rung))
244+
234245
combined_satins.append(satin_rails + segment_geoms)
235246
return combined_satins
236247

248+
def _adjust_closed_path_starting_point(self, rails, rung):
249+
# closed paths may need adjustments of the starting point
250+
rail1 = rails[0].geoms[0]
251+
rail2 = rails[0].geoms[1]
252+
if rail1.coords[0] == rail1.coords[-1]:
253+
rail1 = self._adjust_rail_starting_point(rail1, rail1.intersection(rung))
254+
rail2 = self._adjust_rail_starting_point(rail2, rail2.intersection(rung))
255+
return MultiLineString([rail1, rail2])
256+
257+
def _adjust_rail_starting_point(self, rail, point):
258+
if point.geom_type == "Point":
259+
position = rail.project(point)
260+
return LineString(roll_linear_ring(rail, position))
261+
return rail
262+
263+
def _fix_single_rail_issue(self, satin_rails, segments, satin_segments, combined_rungs):
264+
# This is a special case where the two satin rails have been combined into one.
265+
# It can happen if we try to convert for example a B with a single satin column
266+
# (it starts and ends at the center).
267+
# ---
268+
# | \
269+
# | /
270+
# |==x
271+
# | \
272+
# | /
273+
# ---
274+
# We can face two situations:
275+
# 1. the rung at the intersection is within the combined_rungs, in this case we need to watch out for a rung which is bridged twice
276+
# 2. the rung isn't within the selection, adjacing bridged segments have only one connecting rung
277+
278+
# Case 1: the rung is within the selection and is bridged twice
279+
intersection = self._fix_single_rail_issue_rung_included(satin_rails, combined_rungs)
280+
if intersection.is_empty:
281+
# Case 2: check for segments with only one adjacent rung
282+
intersection = self._fix_single_rail_issue_rung_excluded(satin_segments, segments, combined_rungs)
283+
if intersection.geom_type == 'MultiPoint':
284+
position = satin_rails.project(intersection.geoms[0])
285+
satin_rails = LineString(roll_linear_ring(satin_rails.geoms[0], position))
286+
return ensure_multi_line_string(split(satin_rails, intersection))
287+
return None
288+
289+
def _fix_single_rail_issue_rung_included(self, satin_rails, combined_rungs):
290+
for rung in combined_rungs:
291+
rung_bridges = []
292+
for bridge, rungs in self.bridged_rungs.items():
293+
if rung in rungs:
294+
rung_bridges.append(1)
295+
if len(rung_bridges) > 1:
296+
rung_geom = snap(self.rungs[rung], satin_rails, 0.001)
297+
return satin_rails.intersection(rung_geom)
298+
return Point()
299+
300+
def _fix_single_rail_issue_rung_excluded(self, satin_segments, segments, combined_rungs):
301+
single_rung_segments = []
302+
for segment_index in set(segments):
303+
geom = satin_segments[segment_index]
304+
segment_rungs = []
305+
for rung_index in combined_rungs:
306+
if geom.distance(self.rungs[rung_index]) < 0.001:
307+
segment_rungs.append(rung_index)
308+
if len(segment_rungs) == 1:
309+
single_rung_segments.append(geom)
310+
if len(single_rung_segments) == 2:
311+
points = []
312+
for seg in single_rung_segments:
313+
segment_end_points = []
314+
for g in seg.geoms:
315+
segment_end_points.extend([g.coords[0], g.coords[-1]])
316+
points.append(segment_end_points)
317+
return MultiPoint(points[0]).intersection(MultiPoint(points[1]))
318+
return Point()
319+
237320
def _get_segments(self, intersection_points): # noqa: C901
238321
'''Combine line sections to satin segments (find the rails that belong together)'''
239322
line_section_multi = MultiLineString(self.line_sections)
@@ -275,9 +358,9 @@ def _get_segments(self, intersection_points): # noqa: C901
275358
if len(rung_list) != 2:
276359
continue
277360
for rung in s_rungs:
361+
if bridge in used_bridges:
362+
continue
278363
if rung in rung_list:
279-
if bridge in used_bridges:
280-
continue
281364
rung1 = rung_list[0]
282365
rung2 = rung_list[1]
283366
segment = self._get_bridged_segment(rung1, rung2, intersection_points, line_section_multi)
@@ -294,6 +377,21 @@ def _get_segments(self, intersection_points): # noqa: C901
294377
# IF users define their rungs well, they won't have a problem if we just ignore these sections
295378
# otherwise they will see some sort of gap, they can close it manually if they want
296379
pass
380+
381+
# create segments for unused bridge segments
382+
unused_bridges = set(self.bridged_rungs.keys()) - set(used_bridges)
383+
if unused_bridges:
384+
for bridge in unused_bridges:
385+
rungs = self.bridged_rungs[bridge]
386+
if len(rungs) != 2:
387+
continue
388+
segment = self._get_bridged_segment(rungs[0], rungs[1], intersection_points, line_section_multi)
389+
if not segment:
390+
continue
391+
satin_segments.append(segment)
392+
rung_segments[rungs[0]].append(segment_index)
393+
rung_segments[rungs[1]].append(segment_index)
394+
segment_index += 1
297395
return rung_segments, satin_segments
298396

299397
def _get_bridged_segment(self, rung1, rung2, intersection_points, line_section_multi):
@@ -485,8 +583,7 @@ def _validate_rungs(self):
485583
# these rungs (possibly) connect two rungs
486584
bridges.append(rung)
487585
elif intersection.geom_type == 'Point':
488-
# half rungs will can mark a bridge endpoint at an open end within the shape
489-
# intersection_points.append(intersection)
586+
# half rungs can mark a bridge endpoint at an open end within the shape
490587
half_rungs.append(rung)
491588
# filter rungs when they are crossing other rungs. They could possibly produce bad line sections
492589
for i, rung in enumerate(rungs):
@@ -511,9 +608,10 @@ def _validate_bridges(self, bridges, intersection_points):
511608
multi_rung = MultiLineString(self.rungs)
512609
# find elements marked as bridges, but don't intersect with any other rung.
513610
# they may be rungs drawn inside of a shape, so let's add them to the rungs and see if they are helpful
514-
for bridge in bridges:
611+
for i, bridge in enumerate(bridges):
515612
rung_intersections = bridge.intersection(multi_rung)
516-
if rung_intersections.is_empty:
613+
bridge_intersections = bridge.intersection(MultiLineString([b for j, b in enumerate(bridges) if j != i]))
614+
if rung_intersections.is_empty and not bridge_intersections.geom_type == "MultiPoint":
517615
# doesn't intersect with any rungs, so it is a rung itself (when bridged)
518616
self.half_rungs.append(len(self.rungs))
519617
self.rungs.append(bridge)
@@ -534,7 +632,6 @@ def _validate_bridges(self, bridges, intersection_points):
534632
distance1 = bridge.project(point1) - 0.1
535633
distance2 = bridge.project(point2) + 0.1
536634
validated_bridges.append(substring(bridge, distance1, distance2))
537-
validated_bridges.append(bridge)
538635
elif rung_intersections.geom_type == "Point":
539636
# bridges a rung within the shape
540637
validated_bridges.append(bridge)

0 commit comments

Comments
 (0)