kopia lustrzana https://github.com/inkstitch/inkstitch
Stroke to Fill: Ignore Small Artifacts (#2678)
* Ignore artifacts * insert one centerline group per fill element * prevent error on elements with fill and strokepull/2703/head
rodzic
2677c30a0f
commit
2e60900b1a
|
@ -3,16 +3,16 @@
|
||||||
# Copyright (c) 2022 Authors
|
# Copyright (c) 2022 Authors
|
||||||
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
|
# 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 inkex.units import convert_unit
|
||||||
from shapely.geometry import (LineString, MultiLineString, MultiPolygon, Point,
|
from shapely.geometry import LineString, MultiLineString, MultiPolygon, Point
|
||||||
Polygon)
|
|
||||||
from shapely.ops import linemerge, nearest_points, split, voronoi_diagram
|
from shapely.ops import linemerge, nearest_points, split, voronoi_diagram
|
||||||
|
|
||||||
from ..elements import FillStitch, Stroke
|
from ..elements import FillStitch, Stroke
|
||||||
from ..i18n import _
|
from ..i18n import _
|
||||||
from ..stitches.running_stitch import running_stitch
|
from ..stitches.running_stitch import running_stitch
|
||||||
from ..svg import get_correction_transform
|
from ..svg import get_correction_transform
|
||||||
|
from ..utils import ensure_multi_line_string
|
||||||
from ..utils.geometry import Point as InkstitchPoint
|
from ..utils.geometry import Point as InkstitchPoint
|
||||||
from ..utils.geometry import line_string_to_point_list
|
from ..utils.geometry import line_string_to_point_list
|
||||||
from .base import InkstitchExtension
|
from .base import InkstitchExtension
|
||||||
|
@ -33,10 +33,7 @@ class FillToStroke(InkstitchExtension):
|
||||||
errormsg(_("Please select one or more fill objects to render the centerline."))
|
errormsg(_("Please select one or more fill objects to render the centerline."))
|
||||||
return
|
return
|
||||||
|
|
||||||
cut_lines = []
|
fill_shapes, cut_lines, cut_line_nodes = self._get_shapes()
|
||||||
fill_shapes = []
|
|
||||||
|
|
||||||
fill_shapes, cut_lines = self._get_shapes()
|
|
||||||
|
|
||||||
if not fill_shapes:
|
if not fill_shapes:
|
||||||
errormsg(_("Please select one or more fill objects to render the centerline."))
|
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
|
# convert user input from mm to px
|
||||||
self.threshold = convert_unit(self.options.threshold_mm, 'px', 'mm')
|
self.threshold = convert_unit(self.options.threshold_mm, 'px', 'mm')
|
||||||
|
|
||||||
# insert centerline group before the first selected element
|
# convert to center line elements and insert into svg
|
||||||
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)
|
|
||||||
|
|
||||||
for element in fill_shapes:
|
for element in fill_shapes:
|
||||||
transform = Transform(get_correction_transform(parent, child=True))
|
self._convert_to_centerline(element, cut_lines)
|
||||||
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
|
# remove cut lines
|
||||||
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)
|
|
||||||
|
|
||||||
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
|
|
||||||
if not self.options.keep_original:
|
if not self.options.keep_original:
|
||||||
self._remove_elements()
|
self._remove_cutlines(cut_line_nodes)
|
||||||
|
|
||||||
def _get_shapes(self):
|
def _get_shapes(self):
|
||||||
fill_shapes = []
|
fill_shapes = []
|
||||||
cut_lines = []
|
cut_lines = []
|
||||||
|
cut_line_nodes = []
|
||||||
for element in self.elements:
|
for element in self.elements:
|
||||||
if isinstance(element, FillStitch):
|
if isinstance(element, FillStitch):
|
||||||
fill_shapes.append(element)
|
fill_shapes.append(element)
|
||||||
elif isinstance(element, Stroke):
|
elif isinstance(element, Stroke):
|
||||||
cut_lines.extend(list(element.as_multi_line_string().geoms))
|
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):
|
def _convert_to_centerline(self, element, cut_lines):
|
||||||
parents = []
|
element_id = element.node.get_id()
|
||||||
for element in self.elements:
|
element_label = element.node.label
|
||||||
# it is possible, that we get one element twice (if it has both, a fill and a stroke)
|
group_name = element_label or element_id
|
||||||
# just ignore the second time
|
|
||||||
try:
|
centerline_group = Group.new(f'{ group_name } { _("center line") }', id=self.uniqueId("centerline_group_"))
|
||||||
parents.append(element.node.getparent())
|
parent = element.node.getparent()
|
||||||
element.node.getparent().remove(element.node)
|
index = parent.index(element.node) + 1
|
||||||
except AttributeError:
|
parent.insert(index, centerline_group)
|
||||||
pass
|
|
||||||
# remove empty groups
|
transform = Transform(get_correction_transform(parent, child=True))
|
||||||
for parent in set(parents):
|
stroke_width = convert_unit(self.options.line_width_mm, 'px', 'mm')
|
||||||
if parent is not None and not parent.getchildren():
|
color = element.node.style('fill')
|
||||||
parent.getparent().remove(parent)
|
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):
|
def _get_high_res_polygon(self, polygon):
|
||||||
# use running stitch method
|
# use running stitch method
|
||||||
|
@ -121,7 +122,7 @@ class FillToStroke(InkstitchExtension):
|
||||||
def _get_centerline(self, polygon):
|
def _get_centerline(self, polygon):
|
||||||
# increase the resolution of the polygon
|
# increase the resolution of the polygon
|
||||||
polygon = self._get_high_res_polygon(polygon)
|
polygon = self._get_high_res_polygon(polygon)
|
||||||
if polygon is isinstance(polygon, MultiPolygon):
|
if polygon is polygon.geom_type == 'MultiPolygon':
|
||||||
return
|
return
|
||||||
|
|
||||||
# generate voronoi centerline
|
# generate voronoi centerline
|
||||||
|
@ -139,14 +140,14 @@ class FillToStroke(InkstitchExtension):
|
||||||
if multilinestring is None:
|
if multilinestring is None:
|
||||||
return
|
return
|
||||||
# simplify polygon
|
# simplify polygon
|
||||||
multilinestring = self._ensure_multilinestring(multilinestring.simplify(0.1))
|
multilinestring = ensure_multi_line_string(multilinestring.simplify(0.1))
|
||||||
if multilinestring is None:
|
if multilinestring is None:
|
||||||
return
|
return
|
||||||
return multilinestring
|
return multilinestring
|
||||||
|
|
||||||
def _get_voronoi_centerline(self, polygon):
|
def _get_voronoi_centerline(self, polygon):
|
||||||
lines = voronoi_diagram(polygon, edges=True).geoms[0]
|
lines = voronoi_diagram(polygon, edges=True).geoms[0]
|
||||||
if not isinstance(lines, MultiLineString):
|
if not lines.geom_type == 'MultiLineString':
|
||||||
return
|
return
|
||||||
multilinestring = []
|
multilinestring = []
|
||||||
for line in lines.geoms:
|
for line in lines.geoms:
|
||||||
|
@ -155,7 +156,14 @@ class FillToStroke(InkstitchExtension):
|
||||||
lines = linemerge(multilinestring)
|
lines = linemerge(multilinestring)
|
||||||
if lines.is_empty:
|
if lines.is_empty:
|
||||||
return
|
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):
|
def _get_start_and_end_points(self, multilinestring):
|
||||||
points = []
|
points = []
|
||||||
|
@ -182,43 +190,49 @@ class FillToStroke(InkstitchExtension):
|
||||||
if lines.is_empty:
|
if lines.is_empty:
|
||||||
lines = None
|
lines = None
|
||||||
else:
|
else:
|
||||||
lines = self._ensure_multilinestring(lines)
|
lines = ensure_multi_line_string(lines)
|
||||||
return lines
|
return lines
|
||||||
|
|
||||||
def _repair_splitted_ends(self, polygon, multilinestring, dead_ends):
|
def _repair_splitted_ends(self, polygon, multilinestring, dead_ends):
|
||||||
lines = list(multilinestring.geoms)
|
lines = list(multilinestring.geoms)
|
||||||
for i, dead_end in enumerate(dead_ends):
|
for i, dead_end in enumerate(dead_ends):
|
||||||
coords = dead_end.coords
|
if dead_end.length > self.threshold:
|
||||||
for j in range(i + 1, len(dead_ends)):
|
continue
|
||||||
common_point = set([coords[0], coords[-1]]).intersection(dead_ends[j].coords)
|
self._join_end(polygon, lines, dead_ends, dead_end, i)
|
||||||
if len(common_point) > 0:
|
return ensure_multi_line_string(linemerge(lines))
|
||||||
# prepare all lines to point to the common point
|
|
||||||
dead_point1 = coords[0]
|
def _join_end(self, polygon, lines, dead_ends, dead_end, index):
|
||||||
if dead_point1 in common_point:
|
coords = dead_end.coords
|
||||||
dead_point1 = coords[-1]
|
for j in range(index + 1, len(dead_ends)):
|
||||||
dead_point2 = dead_ends[j].coords[0]
|
if dead_ends[j].length > self.threshold:
|
||||||
if dead_point2 in common_point:
|
continue
|
||||||
dead_point2 = dead_ends[j].coords[-1]
|
common_point = set([coords[0], coords[-1]]).intersection(dead_ends[j].coords)
|
||||||
end_line = LineString([dead_point1, dead_point2])
|
if len(common_point) > 0:
|
||||||
if polygon.covers(end_line):
|
dead_point1 = coords[0]
|
||||||
dead_end_center_point = end_line.centroid
|
if dead_point1 in common_point:
|
||||||
else:
|
dead_point1 = coords[-1]
|
||||||
continue
|
dead_point2 = dead_ends[j].coords[0]
|
||||||
lines.append(LineString([dead_end_center_point, list(common_point)[0]]))
|
if dead_point2 in common_point:
|
||||||
if dead_end in lines:
|
dead_point2 = dead_ends[j].coords[-1]
|
||||||
lines.remove(dead_end)
|
end_line = LineString([dead_point1, dead_point2])
|
||||||
if dead_ends[j] in lines:
|
if polygon.covers(end_line):
|
||||||
lines.remove(dead_ends[j])
|
dead_end_center_point = end_line.centroid
|
||||||
|
else:
|
||||||
continue
|
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):
|
def _close_gaps(self, lines, cut_lines):
|
||||||
snaped_lines = []
|
snaped_lines = []
|
||||||
lines = MultiLineString(lines)
|
lines = MultiLineString(lines)
|
||||||
for i, line in enumerate(lines.geoms):
|
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
|
# 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)
|
coords = list(line.coords)
|
||||||
start = Point(coords[0])
|
start = Point(coords[0])
|
||||||
end = Point(coords[-1])
|
end = Point(coords[-1])
|
||||||
|
@ -243,15 +257,26 @@ class FillToStroke(InkstitchExtension):
|
||||||
new_point = start_point - direction * distance
|
new_point = start_point - direction * distance
|
||||||
return new_point
|
return new_point
|
||||||
|
|
||||||
def _ensure_multilinestring(self, lines):
|
def _remove_cutlines(self, cut_line_nodes):
|
||||||
if not isinstance(lines, MultiLineString):
|
for cut_line in cut_line_nodes:
|
||||||
lines = MultiLineString([lines])
|
# it is possible, that we get one element twice (if it has both, a fill and a stroke)
|
||||||
return lines
|
# 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):
|
def _insert_elements(self, lines, parent, index, element_id, element_label, transform, style):
|
||||||
for line in lines:
|
replace = False if len(lines) > 1 or self.options.keep_original else True
|
||||||
d = "M "
|
for i, line in enumerate(lines):
|
||||||
for coord in line.coords:
|
line_id = element_id if replace else self.uniqueId(f"{ element_id }_")
|
||||||
d += "%s,%s " % (coord[0], coord[1])
|
centerline_element = PathElement(
|
||||||
centerline_element = PathElement(d=d, style=style, transform=str(transform))
|
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)
|
parent.insert(index, centerline_element)
|
||||||
|
|
Ładowanie…
Reference in New Issue