summaryrefslogtreecommitdiff
path: root/lib/extensions/fill_to_stroke.py
diff options
context:
space:
mode:
authorKaalleen <36401965+kaalleen@users.noreply.github.com>2024-01-25 17:59:27 +0100
committerGitHub <noreply@github.com>2024-01-25 17:59:27 +0100
commit2e60900b1ae4355762f7b59141c1c47f6ffcbc66 (patch)
treef857feeaa4c8711f9abc69270aad03ffd6b13385 /lib/extensions/fill_to_stroke.py
parent2677c30a0f210d14586c83016f45e5a75fb9d647 (diff)
Stroke to Fill: Ignore Small Artifacts (#2678)
* Ignore artifacts * insert one centerline group per fill element * prevent error on elements with fill and stroke
Diffstat (limited to 'lib/extensions/fill_to_stroke.py')
-rw-r--r--lib/extensions/fill_to_stroke.py213
1 files changed, 119 insertions, 94 deletions
diff --git a/lib/extensions/fill_to_stroke.py b/lib/extensions/fill_to_stroke.py
index 28c1f651..c33ede3d 100644
--- a/lib/extensions/fill_to_stroke.py
+++ b/lib/extensions/fill_to_stroke.py
@@ -3,16 +3,16 @@
# Copyright (c) 2022 Authors
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
-from inkex import Boolean, Group, PathElement, Transform, errormsg
+from inkex import Boolean, Group, Path, PathElement, Transform, errormsg
from inkex.units import convert_unit
-from shapely.geometry import (LineString, MultiLineString, MultiPolygon, Point,
- Polygon)
+from shapely.geometry import LineString, MultiLineString, MultiPolygon, Point
from shapely.ops import linemerge, nearest_points, split, voronoi_diagram
from ..elements import FillStitch, Stroke
from ..i18n import _
from ..stitches.running_stitch import running_stitch
from ..svg import get_correction_transform
+from ..utils import ensure_multi_line_string
from ..utils.geometry import Point as InkstitchPoint
from ..utils.geometry import line_string_to_point_list
from .base import InkstitchExtension
@@ -33,10 +33,7 @@ class FillToStroke(InkstitchExtension):
errormsg(_("Please select one or more fill objects to render the centerline."))
return
- cut_lines = []
- fill_shapes = []
-
- fill_shapes, cut_lines = self._get_shapes()
+ fill_shapes, cut_lines, cut_line_nodes = self._get_shapes()
if not fill_shapes:
errormsg(_("Please select one or more fill objects to render the centerline."))
@@ -45,67 +42,71 @@ class FillToStroke(InkstitchExtension):
# convert user input from mm to px
self.threshold = convert_unit(self.options.threshold_mm, 'px', 'mm')
- # insert centerline group before the first selected element
- first = fill_shapes[0].node
- parent = first.getparent()
- index = parent.index(first) + 1
- centerline_group = Group.new("Centerline Group", id=self.uniqueId("centerline_group_"))
- parent.insert(index, centerline_group)
-
+ # convert to center line elements and insert into svg
for element in fill_shapes:
- transform = Transform(get_correction_transform(parent, child=True))
- stroke_width = convert_unit(self.options.line_width_mm, 'px', 'mm')
- color = element.node.style('fill')
- style = f"fill:none;stroke:{ color };stroke-width:{ stroke_width }"
-
- multipolygon = element.shape
- for cut_line in cut_lines:
- split_polygon = split(multipolygon, cut_line)
- poly = [polygon for polygon in split_polygon.geoms if isinstance(polygon, Polygon)]
- multipolygon = MultiPolygon(poly)
-
- lines = []
-
- for polygon in multipolygon.geoms:
- multilinestring = self._get_centerline(polygon)
- if multilinestring is None:
- continue
- lines.extend(multilinestring.geoms)
+ self._convert_to_centerline(element, cut_lines)
- if self.options.close_gaps:
- lines = self._close_gaps(lines, cut_lines)
-
- # insert new elements
- self._insert_elements(lines, centerline_group, index, transform, style)
-
- # clean up
+ # remove cut lines
if not self.options.keep_original:
- self._remove_elements()
+ self._remove_cutlines(cut_line_nodes)
def _get_shapes(self):
fill_shapes = []
cut_lines = []
+ cut_line_nodes = []
for element in self.elements:
if isinstance(element, FillStitch):
fill_shapes.append(element)
elif isinstance(element, Stroke):
cut_lines.extend(list(element.as_multi_line_string().geoms))
- return fill_shapes, cut_lines
+ cut_line_nodes.append(element.node)
+ return fill_shapes, cut_lines, cut_line_nodes
- def _remove_elements(self):
- parents = []
- for element in self.elements:
- # it is possible, that we get one element twice (if it has both, a fill and a stroke)
- # just ignore the second time
- try:
- parents.append(element.node.getparent())
- element.node.getparent().remove(element.node)
- except AttributeError:
- pass
- # remove empty groups
- for parent in set(parents):
- if parent is not None and not parent.getchildren():
- parent.getparent().remove(parent)
+ def _convert_to_centerline(self, element, cut_lines):
+ element_id = element.node.get_id()
+ element_label = element.node.label
+ group_name = element_label or element_id
+
+ centerline_group = Group.new(f'{ group_name } { _("center line") }', id=self.uniqueId("centerline_group_"))
+ parent = element.node.getparent()
+ index = parent.index(element.node) + 1
+ parent.insert(index, centerline_group)
+
+ transform = Transform(get_correction_transform(parent, child=True))
+ stroke_width = convert_unit(self.options.line_width_mm, 'px', 'mm')
+ color = element.node.style('fill')
+ style = f"fill:none;stroke:{ color };stroke-width:{ stroke_width }"
+
+ multipolygon = element.shape
+ multipolygon = self._apply_cut_lines(cut_lines, multipolygon)
+
+ lines = self._get_lines(multipolygon)
+
+ if self.options.close_gaps:
+ lines = self._close_gaps(lines, cut_lines)
+
+ # do not use a group in case there is only one line
+ if len(lines) <= 1:
+ parent.remove(centerline_group)
+ centerline_group = parent
+
+ # clean up
+ if not self.options.keep_original:
+ parent.remove(element.node)
+
+ # insert new elements
+ self._insert_elements(lines, centerline_group, index, element_id, element_label, transform, style)
+
+ def _get_lines(self, multipolygon):
+ lines = []
+ for polygon in multipolygon.geoms:
+ if polygon.area < 0.5:
+ continue
+ multilinestring = self._get_centerline(polygon)
+ if multilinestring is None:
+ continue
+ lines.extend(multilinestring.geoms)
+ return lines
def _get_high_res_polygon(self, polygon):
# use running stitch method
@@ -121,7 +122,7 @@ class FillToStroke(InkstitchExtension):
def _get_centerline(self, polygon):
# increase the resolution of the polygon
polygon = self._get_high_res_polygon(polygon)
- if polygon is isinstance(polygon, MultiPolygon):
+ if polygon is polygon.geom_type == 'MultiPolygon':
return
# generate voronoi centerline
@@ -139,14 +140,14 @@ class FillToStroke(InkstitchExtension):
if multilinestring is None:
return
# simplify polygon
- multilinestring = self._ensure_multilinestring(multilinestring.simplify(0.1))
+ multilinestring = ensure_multi_line_string(multilinestring.simplify(0.1))
if multilinestring is None:
return
return multilinestring
def _get_voronoi_centerline(self, polygon):
lines = voronoi_diagram(polygon, edges=True).geoms[0]
- if not isinstance(lines, MultiLineString):
+ if not lines.geom_type == 'MultiLineString':
return
multilinestring = []
for line in lines.geoms:
@@ -155,7 +156,14 @@ class FillToStroke(InkstitchExtension):
lines = linemerge(multilinestring)
if lines.is_empty:
return
- return self._ensure_multilinestring(lines)
+ return ensure_multi_line_string(lines)
+
+ def _apply_cut_lines(self, cut_lines, multipolygon):
+ for cut_line in cut_lines:
+ split_polygon = split(multipolygon, cut_line)
+ poly = [polygon for polygon in split_polygon.geoms if polygon.geom_type == 'Polygon']
+ multipolygon = MultiPolygon(poly)
+ return multipolygon
def _get_start_and_end_points(self, multilinestring):
points = []
@@ -182,43 +190,49 @@ class FillToStroke(InkstitchExtension):
if lines.is_empty:
lines = None
else:
- lines = self._ensure_multilinestring(lines)
+ lines = ensure_multi_line_string(lines)
return lines
def _repair_splitted_ends(self, polygon, multilinestring, dead_ends):
lines = list(multilinestring.geoms)
for i, dead_end in enumerate(dead_ends):
- coords = dead_end.coords
- for j in range(i + 1, len(dead_ends)):
- common_point = set([coords[0], coords[-1]]).intersection(dead_ends[j].coords)
- if len(common_point) > 0:
- # prepare all lines to point to the common point
- dead_point1 = coords[0]
- if dead_point1 in common_point:
- dead_point1 = coords[-1]
- dead_point2 = dead_ends[j].coords[0]
- if dead_point2 in common_point:
- dead_point2 = dead_ends[j].coords[-1]
- end_line = LineString([dead_point1, dead_point2])
- if polygon.covers(end_line):
- dead_end_center_point = end_line.centroid
- else:
- continue
- lines.append(LineString([dead_end_center_point, list(common_point)[0]]))
- if dead_end in lines:
- lines.remove(dead_end)
- if dead_ends[j] in lines:
- lines.remove(dead_ends[j])
+ if dead_end.length > self.threshold:
+ continue
+ self._join_end(polygon, lines, dead_ends, dead_end, i)
+ return ensure_multi_line_string(linemerge(lines))
+
+ def _join_end(self, polygon, lines, dead_ends, dead_end, index):
+ coords = dead_end.coords
+ for j in range(index + 1, len(dead_ends)):
+ if dead_ends[j].length > self.threshold:
+ continue
+ common_point = set([coords[0], coords[-1]]).intersection(dead_ends[j].coords)
+ if len(common_point) > 0:
+ dead_point1 = coords[0]
+ if dead_point1 in common_point:
+ dead_point1 = coords[-1]
+ dead_point2 = dead_ends[j].coords[0]
+ if dead_point2 in common_point:
+ dead_point2 = dead_ends[j].coords[-1]
+ end_line = LineString([dead_point1, dead_point2])
+ if polygon.covers(end_line):
+ dead_end_center_point = end_line.centroid
+ else:
continue
- return self._ensure_multilinestring(linemerge(lines))
+ lines.append(LineString([dead_end_center_point, list(common_point)[0]]))
+ if dead_end in lines:
+ lines.remove(dead_end)
+ if dead_ends[j] in lines:
+ lines.remove(dead_ends[j])
+ continue
def _close_gaps(self, lines, cut_lines):
snaped_lines = []
lines = MultiLineString(lines)
for i, line in enumerate(lines.geoms):
- # for each cutline check if a the line starts or ends close to it
+ # for each cutline check if a line starts or ends close to it
# if so extend the line at the start/end for the distance of the nearest point and snap it to that other line
- # we do not want to snap it to the rest of the lines directly, this could push the connection point into a unwanted direction
+ # we do not want to snap it to the rest of the lines directly, this could push the connection point into an unwanted direction
coords = list(line.coords)
start = Point(coords[0])
end = Point(coords[-1])
@@ -243,15 +257,26 @@ class FillToStroke(InkstitchExtension):
new_point = start_point - direction * distance
return new_point
- def _ensure_multilinestring(self, lines):
- if not isinstance(lines, MultiLineString):
- lines = MultiLineString([lines])
- return lines
+ def _remove_cutlines(self, cut_line_nodes):
+ for cut_line in cut_line_nodes:
+ # it is possible, that we get one element twice (if it has both, a fill and a stroke)
+ # this means that we already removed it from the svg and we can ignore the error.
+ try:
+ cut_line.getparent().remove(cut_line)
+ except AttributeError:
+ pass
- def _insert_elements(self, lines, parent, index, transform, style):
- for line in lines:
- d = "M "
- for coord in line.coords:
- d += "%s,%s " % (coord[0], coord[1])
- centerline_element = PathElement(d=d, style=style, transform=str(transform))
+ def _insert_elements(self, lines, parent, index, element_id, element_label, transform, style):
+ replace = False if len(lines) > 1 or self.options.keep_original else True
+ for i, line in enumerate(lines):
+ line_id = element_id if replace else self.uniqueId(f"{ element_id }_")
+ centerline_element = PathElement(
+ id=line_id,
+ d=str(Path(line.coords)),
+ style=style,
+ transform=str(transform)
+ )
+ if element_label is not None:
+ label = element_label if replace else f"{ element_label }_{ i }"
+ centerline_element.label = label
parent.insert(index, centerline_element)