Fill Centerline (#1722)

pull/2020/head
Kaalleen 2023-01-15 12:31:27 +01:00 zatwierdzone przez GitHub
rodzic 748ed7368e
commit 4156c4adb4
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
3 zmienionych plików z 254 dodań i 0 usunięć

Wyświetl plik

@ -18,6 +18,7 @@ from .cutwork_segmentation import CutworkSegmentation
from .density_map import DensityMap
from .duplicate_params import DuplicateParams
from .embroider_settings import EmbroiderSettings
from .fill_to_stroke import FillToStroke
from .flip import Flip
from .generate_palette import GeneratePalette
from .global_commands import GlobalCommands
@ -71,6 +72,7 @@ __all__ = extensions = [StitchPlanPreview,
ConvertToSatin,
ConvertToStroke,
JumpToStroke,
FillToStroke,
CutSatin,
AutoSatin,
AutoRun,

Wyświetl plik

@ -0,0 +1,213 @@
# Authors: see git history
#
# 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, errormsg
from inkex.units import convert_unit
from shapely.geometry import LineString, MultiLineString, MultiPolygon, Polygon
from shapely.ops import linemerge, split, voronoi_diagram
from ..elements import FillStitch, Stroke
from ..i18n import _
from ..stitches.running_stitch import running_stitch
from ..svg import PIXELS_PER_MM, get_correction_transform
from ..utils.geometry import line_string_to_point_list
from .base import InkstitchExtension
class FillToStroke(InkstitchExtension):
def __init__(self, *args, **kwargs):
InkstitchExtension.__init__(self, *args, **kwargs)
self.arg_parser.add_argument("--options", dest="options", type=str, default="")
self.arg_parser.add_argument("--info", dest="help", type=str, default="")
self.arg_parser.add_argument("-t", "--threshold_mm", dest="threshold_mm", type=float, default=10)
self.arg_parser.add_argument("-o", "--keep_original", dest="keep_original", type=Boolean, default=False)
self.arg_parser.add_argument("-d", "--dashed_line", dest="dashed_line", type=Boolean, default=True)
self.arg_parser.add_argument("-w", "--line_width_mm", dest="line_width_mm", type=float, default=1)
def effect(self):
if not self.svg.selected or not self.get_elements():
errormsg(_("Please select one or more fill objects to render the centerline."))
return
cut_lines = []
fill_shapes = []
fill_shapes, cut_lines = self._get_shapes()
if not fill_shapes:
errormsg(_("Please select one or more fill objects to render the centerline."))
return
# convert user input from mm to px
self.threshold = self.options.threshold_mm * PIXELS_PER_MM
self.line_width = self.options.line_width_mm * PIXELS_PER_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)
transform = get_correction_transform(first)
for element in fill_shapes:
transform = element.node.transform @ transform
dashed = "stroke-dasharray:12,1.5;" if self.options.dashed_line else ""
stroke_width = convert_unit(self.line_width, self.svg.unit)
color = element.node.style('fill')
style = "fill:none;stroke:%s;stroke-width:%s;%s" % (color, stroke_width, dashed)
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)
for polygon in multipolygon.geoms:
multilinestring = self._get_centerline(polygon)
if multilinestring is None:
continue
# insert new elements
self._insert_elements(multilinestring, centerline_group, index, transform, style)
# clean up
if not self.options.keep_original:
self._remove_elements()
def _get_shapes(self):
fill_shapes = []
cut_lines = []
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
def _remove_elements(self):
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:
element.node.getparent().remove(element.node)
except AttributeError:
pass
def _get_high_res_polygon(self, polygon):
# use running stitch method
runs = [running_stitch(line_string_to_point_list(polygon.exterior), 1, 0.1)]
if len(runs[0]) < 3:
return
for interior in polygon.interiors:
runs.append(running_stitch(line_string_to_point_list(interior), 1, 0.1))
return MultiPolygon([(runs[0], runs[1:])])
def _get_centerline(self, polygon):
# increase the resolution of the polygon
polygon = self._get_high_res_polygon(polygon)
if polygon is isinstance(polygon, MultiPolygon):
return
# generate voronoi centerline
multilinestring = self._get_voronoi_centerline(polygon)
if multilinestring is None:
return
# dead ends
dead_ends = self._get_dead_end_lines(multilinestring)
# avoid the splitting of line ends
multilinestring = self._repair_splitted_ends(polygon, multilinestring, dead_ends)
# update dead ends
dead_ends = self._get_dead_end_lines(multilinestring)
# filter small dead ends
multilinestring = self._filter_short_dead_ends(multilinestring, dead_ends)
if multilinestring is None:
return
# simplify polygon
multilinestring = self._ensure_multilinestring(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):
return
multilinestring = []
for line in lines.geoms:
if polygon.covers(line):
multilinestring.append(line)
lines = linemerge(multilinestring)
if lines.is_empty:
return
return self._ensure_multilinestring(lines)
def _get_start_and_end_points(self, multilinestring):
points = []
for line in multilinestring.geoms:
points.extend(line.coords[::len(line.coords)-1])
return points
def _get_dead_end_lines(self, multilinestring):
start_and_end_points = self._get_start_and_end_points(multilinestring)
dead_ends = []
for line in multilinestring.geoms:
num_neighbours_start = start_and_end_points.count(line.coords[0]) - 1
num_neighbours_end = start_and_end_points.count(line.coords[-1]) - 1
if num_neighbours_start == 0 or num_neighbours_end == 0:
dead_ends.append(line)
return dead_ends
def _filter_short_dead_ends(self, multilinestring, dead_ends):
lines = list(multilinestring.geoms)
for i, line in enumerate(multilinestring.geoms):
if line in dead_ends and line.length < self.threshold:
lines.remove(line)
lines = linemerge(lines)
if lines.is_empty:
lines = None
else:
lines = self._ensure_multilinestring(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])
continue
return self._ensure_multilinestring(linemerge(lines))
def _ensure_multilinestring(self, lines):
if not isinstance(lines, MultiLineString):
lines = MultiLineString([lines])
return lines
def _insert_elements(self, lines, parent, index, transform, style):
for line in lines.geoms:
d = "M "
for coord in line.coords:
d += "%s,%s " % (coord[0], coord[1])
centerline_element = PathElement(d=d, style=style, transform=str(transform))
parent.insert(index, centerline_element)

Wyświetl plik

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension translationdomain="inkstitch" xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Fill to Stroke</name>
<id>org.inkstitch.fill_to_stroke</id>
<param name="extension" type="string" gui-hidden="true">fill_to_stroke</param>
<effect>
<object-type>all</object-type>
<effects-menu>
<submenu name="Ink/Stitch" translatable="no">
<submenu name="Tools: Stroke" />
</submenu>
</effects-menu>
</effect>
<param name="options" type="notebook">
<page name="options" gui-text="Options">
<param name="keep_original" type="boolean" gui-text="Keep original">false</param>
<param name="threshold_mm" type="float" precision="2" min="0" max="500"
gui-text="Threshold for dead ends (mm)"
gui-description="Deletes small lines. A good value in most cases is the approximate line width of the original shape">10</param>
<param name="dashed_line" type="boolean" gui-text="Dashed line">true</param>
<param name="line_width_mm" type="float" gui-text="Line width (mm)" min="0" max="15" precision="2">0.26</param>
</page>
<page name="info" gui-text="Help">
<label appearance="header">Fill to Stroke</label>
<label>Fill outlines never look nice when embroidered - but it is a lot of work to convert a fill outline to a satin column or a running stitch.
This tool helps you with this operation.</label>
<label>It is comparable to the Inkscape functionality of Path > Trace bitmap > Centerline tracing (- and has similar issues.)
But instead of converting raster graphics, it will find the centerline of vector based objects with a fill.</label>
<spacer />
<label>You can improve the result by defining cut lines.</label>
<spacer />
<label>Get more information on our website</label>
<label appearance="url">https://inkstitch.org/docs/stroke-tools/#fill-to-stroke</label>
</page>
</param>
<script>
{{ command_tag | safe }}
</script>
</inkscape-extension>