diff options
| author | Kaalleen <36401965+kaalleen@users.noreply.github.com> | 2025-01-08 17:16:38 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-01-08 17:16:38 +0100 |
| commit | b8f5241755466b2432f89a11c2d5ccdf5a9ae3f2 (patch) | |
| tree | 22e8dd20ae7579805f0cabef7891ebecc3f3597b | |
| parent | cf579e4f3ccbe13152c81957b43d4c34190529c3 (diff) | |
Fill to satin bridged rungs (#3412)
| -rw-r--r-- | lib/extensions/fill_to_satin.py | 270 |
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 |
