summaryrefslogtreecommitdiff
path: root/lib/extensions/fill_to_satin.py
diff options
context:
space:
mode:
authorKaalleen <36401965+kaalleen@users.noreply.github.com>2025-01-08 17:16:38 +0100
committerGitHub <noreply@github.com>2025-01-08 17:16:38 +0100
commitb8f5241755466b2432f89a11c2d5ccdf5a9ae3f2 (patch)
tree22e8dd20ae7579805f0cabef7891ebecc3f3597b /lib/extensions/fill_to_satin.py
parentcf579e4f3ccbe13152c81957b43d4c34190529c3 (diff)
Fill to satin bridged rungs (#3412)
Diffstat (limited to 'lib/extensions/fill_to_satin.py')
-rw-r--r--lib/extensions/fill_to_satin.py270
1 files changed, 144 insertions, 126 deletions
diff --git a/lib/extensions/fill_to_satin.py b/lib/extensions/fill_to_satin.py
index 4a9f2904..d04d2729 100644
--- a/lib/extensions/fill_to_satin.py
+++ b/lib/extensions/fill_to_satin.py
@@ -27,80 +27,76 @@ class FillToSatin(InkstitchExtension):
self.arg_parser.add_argument("--zigzag", dest="zigzag", type=Boolean, default=False)
self.arg_parser.add_argument("--keep_originals", dest="keep_originals", type=Boolean, default=False)
- # geometries
- self.line_sections = []
- self.selected_rungs = []
- self.rungs = [] # selection of valid rungs for the specific fill
-
- # relations
- self.rung_sections = defaultdict(list)
- self.section_rungs = defaultdict(list)
- self.bridged_sections = []
- self.rung_segments = {}
-
- self.satin_index = 1
+ self.satin_index = 0
def effect(self):
if not self.svg.selected or not self.get_elements():
self.print_error()
return
- fill_elements = self._get_shapes()
- if not fill_elements or not self.selected_rungs:
+ fill_elements, selected_rungs = self._get_shapes()
+ if not fill_elements or not selected_rungs:
self.print_error()
return
+ settings = {
+ 'skip_end_section': self.options.skip_end_section,
+ }
+
for fill_element in fill_elements:
fill_shape = fill_element.shape
fill_linestrings = self._fill_to_linestrings(fill_shape)
for linestrings in fill_linestrings:
- # Reset variables
- self.rungs = []
- self.line_sections = []
- self.rung_sections = defaultdict(list)
- self.section_rungs = defaultdict(list)
- self.bridged_sections = []
- self.rung_segments = {}
-
- intersection_points, bridges = self._validate_rungs(linestrings)
-
- self._generate_line_sections(linestrings)
- self._define_relations(bridges)
-
- if len(self.line_sections) == 2 and self.line_sections[0].distance(self.line_sections[1]) > 0:
- # there is only one segment, add it directly
- rails = [MultiLineString([self.line_sections[0], self.line_sections[1]])]
- rungs = [ensure_multi_line_string(self.rungs[0])]
- self._insert_satins(fill_element, [rails + rungs])
- continue
- else:
- rung_segments, satin_segments = self._get_segments(intersection_points)
+ fill_to_satin = FillElementToSatin(self.svg, settings, fill_element, fill_shape, linestrings, selected_rungs)
+ satins = fill_to_satin.convert_to_satin()
+ self._insert_satins(fill_element, satins)
- if len(self.rung_sections) == 2 and self.rung_sections[0] == self.rung_sections[1]:
- combined_satins = self._get_two_rung_circle_geoms(rung_segments, satin_segments)
- else:
- combined_satins = self._get_satin_geoms(rung_segments, satin_segments)
+ self._remove_originals()
- self._insert_satins(fill_element, combined_satins)
+ def _get_shapes(self):
+ '''Filter selected elements. Take rungs and fills.'''
+ fill_elements = []
+ selected_rungs = []
+ nodes = []
+ warned = False
+ for element in self.elements:
+ if element.node in nodes and not warned:
+ self.print_error(
+ (f'{element.node.label} ({element.node.get_id()}): ' + _("This element has a fill and a stroke.\n\n"
+ "Rungs only have a stroke color and fill elements a fill color."))
+ )
+ warned = True
+ nodes.append(element.node)
+ if isinstance(element, FillStitch):
+ fill_elements.append(element)
+ elif isinstance(element, Stroke):
+ selected_rungs.extend(list(element.as_multi_line_string().geoms))
+ return fill_elements, selected_rungs
- self._remove_originals()
+ def _fill_to_linestrings(self, fill_shape):
+ '''Takes a fill shape (Multipolygon) and returns the shape as a list of linestrings'''
+ fill_linestrings = []
+ for polygon in fill_shape.geoms:
+ linestrings = ensure_multi_line_string(polygon.boundary, 1)
+ fill_linestrings.append(list(linestrings.geoms))
+ return fill_linestrings
- def _insert_satins(self, fill_element, combined_satins):
+ def _insert_satins(self, fill_element, satins):
'''Insert satin elements into the document'''
- if not combined_satins:
+ if not satins:
return
group = fill_element.node.getparent()
index = group.index(fill_element.node) + 1
transform = get_correction_transform(fill_element.node)
style = f'stroke: {fill_element.color}; fill: none; stroke-width: {self.svg.viewport_to_unit("1px")};'
- if len(combined_satins) > 1:
+ if len(satins) > 1:
new_group = Group()
group.insert(index, new_group)
group = new_group
group.label = _("Satin Group")
index = 0
- for i, satin in enumerate(combined_satins):
+ for i, satin in enumerate(satins):
node = PathElement()
d = ""
for segment in satin:
@@ -127,6 +123,54 @@ class FillToSatin(InkstitchExtension):
for element in self.elements:
element.node.getparent().remove(element.node)
+ def print_error(self, message=_("Please select a fill object and rungs.")):
+ '''We did not receive the rigth elements, inform user'''
+ app = AbortMessageApp(
+ message,
+ _("https://inkstitch.org/docs/satin-tools/#fill-to-satin")
+ )
+ app.MainLoop()
+
+
+class FillElementToSatin:
+ def __init__(self, svg, settings, fill_element, fill_shape, linestrings, selected_rungs):
+ self.svg = svg
+ self.settings = settings
+ self.fill_element = fill_element
+ self.fill_shape = fill_shape
+ self.linestrings = linestrings
+ self.selected_rungs = selected_rungs
+
+ self.rungs = []
+ self.line_sections = []
+ self.rung_sections = defaultdict(list)
+ self.section_rungs = defaultdict(list)
+ self.bridged_rungs = defaultdict(list)
+ self.used_bridged_rungs = []
+ self.rung_segments = {}
+
+ def convert_to_satin(self):
+ intersection_points, bridges = self._validate_rungs()
+
+ self._generate_line_sections()
+ self._define_relations(bridges)
+
+ if len(self.line_sections) == 2 and self.line_sections[0].distance(self.line_sections[1]) > 0:
+ # there is only one segment, add it directly
+ rails = [MultiLineString([self.line_sections[0], self.line_sections[1]])]
+ rungs = [ensure_multi_line_string(self.rungs[0])]
+ self._insert_satins([rails + rungs])
+ return
+ else:
+ rung_segments, satin_segments = self._get_segments(intersection_points)
+
+ if len(self.rung_sections) == 2 and self.rung_sections[0] == self.rung_sections[1]:
+ combined_satins = self._get_two_rung_circle_geoms(rung_segments, satin_segments)
+ else:
+ combined_satins = self._get_satin_geoms(rung_segments, satin_segments)
+
+ return combined_satins
+
def _get_two_rung_circle_geoms(self, rung_segments, satin_segments):
'''Imagine a donut with two rungs: this is a special case where all segments connect to the very same two rungs'''
combined = defaultdict(list)
@@ -163,8 +207,9 @@ class FillToSatin(InkstitchExtension):
for segment_index in set(segments):
segment_geoms.extend(list(satin_segments[segment_index].geoms))
satin_rails = ensure_multi_line_string(linemerge(segment_geoms))
+ if len(satin_rails.geoms) != 2:
+ continue
satin_rails = [self._adjust_rail_direction(satin_rails)]
-
segment_geoms = []
for rung_index in set(combined_rungs[i]):
rung = self.rungs[rung_index]
@@ -179,6 +224,7 @@ class FillToSatin(InkstitchExtension):
line_section_multi = MultiLineString(self.line_sections)
rung_segments = defaultdict(list)
satin_segments = []
+ used_bridges = []
segment_index = 0
finished_sections = []
@@ -187,7 +233,7 @@ class FillToSatin(InkstitchExtension):
continue
s_rungs = self.section_rungs[i]
if len(s_rungs) == 1:
- if self.options.skip_end_section and len(self.rungs) > 1:
+ if self.settings['skip_end_section'] and len(self.rungs) > 1:
continue
segment = self._get_end_segment(section)
satin_segments.append(segment)
@@ -207,20 +253,35 @@ class FillToSatin(InkstitchExtension):
rung_segments[rung].append(segment_index)
segment_index += 1
finished_sections.extend([i, connect_index])
-
- elif i in self.bridged_sections:
- segment = self._get_bridged_segment(section, s_rungs, intersection_points, line_section_multi)
- if segment:
- satin_segments.append(segment)
- for rung in s_rungs:
- rung_segments[rung].append(segment_index)
- segment_index += 1
- finished_sections.append(i)
else:
- # sections with multiple rungs, open ends, not bridged
- # IF users define their rungs well, they won't have a problem if we just ignore these sections
- # otherwise they will see some sort of gap, they can close it manually if they want
- pass
+ for bridge, rung_list in self.bridged_rungs.items():
+ if len(rung_list) != 2:
+ continue
+ for rung in s_rungs:
+ if rung in rung_list:
+ if bridge in used_bridges:
+ continue
+ points1 = intersection_points[rung_list[0]].geoms
+ points2 = intersection_points[rung_list[1]].geoms
+ segment1 = LineString([points1[0], points2[0]])
+ segment2 = LineString([points1[1], points2[1]])
+ if segment1.intersects(segment2):
+ segment1 = LineString([points1[0], points2[1]])
+ segment2 = LineString([points1[1], points2[0]])
+ segment1 = snap(segment1, line_section_multi, 0.0001)
+ segment2 = snap(segment2, line_section_multi, 0.0001)
+ segment = MultiLineString([segment1, segment2])
+ satin_segments.append(segment)
+ rung_segments[rung_list[0]].append(segment_index)
+ rung_segments[rung_list[1]].append(segment_index)
+ segment_index += 1
+ finished_sections.append(i)
+ used_bridges.append(bridge)
+ else:
+ # sections with multiple rungs, open ends, not bridged
+ # IF users define their rungs well, they won't have a problem if we just ignore these sections
+ # otherwise they will see some sort of gap, they can close it manually if they want
+ pass
return rung_segments, satin_segments
def _get_end_segment(self, section):
@@ -256,22 +317,6 @@ class FillToSatin(InkstitchExtension):
segment = MultiLineString([section, section2])
return connect_index, segment
- def _get_bridged_segment(self, section, s_rungs, intersection_points, line_section_multi):
- segment = None
- bridge_points = []
- # create bridge
- for rung in s_rungs:
- rung_points = intersection_points[rung].geoms
- for point in rung_points:
- if point.distance(section) > 0.01:
- bridge_points.append(point)
- if len(bridge_points) == 2:
- rung = self.rungs[s_rungs[0]]
- bridge = LineString(bridge_points)
- bridge = snap(bridge, line_section_multi, 0.0001)
- segment = MultiLineString([section, bridge])
- return segment
-
def _get_connected_section(self, index, s_rungs):
rung_section_list = []
for rung in s_rungs:
@@ -327,10 +372,11 @@ class FillToSatin(InkstitchExtension):
'''Finds the segments which are neighboring this segment'''
return {rung: segments for rung, segments in self.rung_segments.items() if segment in segments and segments_in != segments}
- def _generate_line_sections(self, fill_linestrings):
+ def _generate_line_sections(self):
'''Splits the fill outline into sections. Splitter is a MultiLineString with all available rungs'''
rungs = MultiLineString(self.rungs)
- for line in fill_linestrings:
+
+ for line in self.linestrings:
sections = list(ensure_multi_line_string(split(line, rungs)).geoms)
if len(sections) > 1:
# merge end and start section
@@ -342,64 +388,36 @@ class FillToSatin(InkstitchExtension):
''' Defines information about the relations between line_sections and rungs
rung_sections: dictionary with rung_index: neighboring sections
section_rungs: dictionary with section_id: neighboring rungs
- bridged_sections: list of sections which the user marked for bridging
+ bridged_rungs: lines which define which segments are to be bridged at intersection points
'''
for i, section in enumerate(self.line_sections):
- if not section.intersection(bridges).is_empty:
- self.bridged_sections.append(i)
for j, rung in enumerate(self.rungs):
if section.distance(rung) < 0.01:
self.section_rungs[i].append(j)
self.rung_sections[j].append(i)
+ for i, bridge in enumerate(bridges):
+ for j, rung in enumerate(self.rungs):
+ if bridge.intersects(rung):
+ self.bridged_rungs[i].append(j)
- def _validate_rungs(self, linestrings):
+ def _validate_rungs(self):
''' Returns only valid rungs and bridge section markers'''
- multi_line_string = MultiLineString(linestrings)
- valid_rungs = []
- bridge_indicators = []
+ multi_line_string = MultiLineString(self.linestrings)
+ bridges = []
intersection_points = []
+ rungs = []
for rung in self.selected_rungs:
intersection = multi_line_string.intersection(rung)
if intersection.geom_type == 'MultiPoint' and len(intersection.geoms) == 2:
- valid_rungs.append(rung)
+ rungs.append(rung)
intersection_points.append(intersection)
- elif intersection.geom_type == 'Point':
- # these rungs help to indicate how the satin section should be connected
- bridge_indicators.append(rung)
- self.rungs = valid_rungs
- return intersection_points, MultiLineString(bridge_indicators)
-
- def _fill_to_linestrings(self, fill_shape):
- '''Takes a fill shape (Multipolygon) and returns the shape as a list of linestrings'''
- fill_linestrings = []
- for polygon in fill_shape.geoms:
- linestrings = ensure_multi_line_string(polygon.boundary, 1)
- fill_linestrings.append(list(linestrings.geoms))
- return fill_linestrings
-
- def _get_shapes(self):
- '''Filter selected elements. Take rungs and fills.'''
- fill_elements = []
- nodes = []
- warned = False
- for element in self.elements:
- if element.node in nodes and not warned:
- self.print_error(
- (f'{element.node.label} ({element.node.get_id()}): ' + _("This element has a fill and a stroke.\n\n"
- "Rungs only have a stroke color and fill elements a fill color."))
- )
- warned = True
- nodes.append(element.node)
- if isinstance(element, FillStitch):
- fill_elements.append(element)
- elif isinstance(element, Stroke):
- self.selected_rungs.extend(list(element.as_multi_line_string().geoms))
- return fill_elements
-
- def print_error(self, message=_("Please select a fill object and rungs.")):
- '''We did not receive the rigth elements, inform user'''
- app = AbortMessageApp(
- message,
- _("https://inkstitch.org/satin-tools#fill-to-satin")
- )
- app.MainLoop()
+ elif intersection.is_empty and rung.within(self.fill_shape):
+ # these rungs (possibly) connect two rungs
+ bridges.append(rung)
+ # filter rungs when they are crossing other rungs. They could possibly produce bad line sections
+ for i, rung in enumerate(rungs):
+ multi_rung = MultiLineString([r for j, r in enumerate(rungs) if j != i])
+ intersection = rung.intersection(multi_rung)
+ if not rung.intersects(multi_rung) or not rung.intersection(multi_rung).intersects(self.fill_shape):
+ self.rungs.append(rung)
+ return intersection_points, bridges