summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKaalleen <36401965+kaalleen@users.noreply.github.com>2025-01-05 12:52:02 +0100
committerGitHub <noreply@github.com>2025-01-05 12:52:02 +0100
commit51ee6c7a08d559d43cd402febe6f9bd4b33c30b0 (patch)
treeb61bbf9f1ff686b917a1b2a0ee9b1a2223508580
parentbe66297da6aec234ef807b20609943f3c3252c2f (diff)
Fill to satin (#3406)
-rw-r--r--icons/inx/fill_to_satin.svg69
-rw-r--r--lib/extensions/__init__.py4
-rw-r--r--lib/extensions/fill_to_satin.py405
-rw-r--r--lib/extensions/fill_to_stroke.py10
-rw-r--r--lib/lettering/font.py2
-rw-r--r--templates/fill_to_satin.xml43
6 files changed, 526 insertions, 7 deletions
diff --git a/icons/inx/fill_to_satin.svg b/icons/inx/fill_to_satin.svg
new file mode 100644
index 00000000..95c9b166
--- /dev/null
+++ b/icons/inx/fill_to_satin.svg
@@ -0,0 +1,69 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xml:space="preserve"
+ width="99.999901mm"
+ height="100mm"
+ version="1.1"
+ style="clip-rule:evenodd;fill-rule:evenodd;image-rendering:optimizeQuality;shape-rendering:geometricPrecision;text-rendering:geometricPrecision"
+ viewBox="0 0 15109 15109.485"
+ id="svg5"
+ sodipodi:docname="fill_to_satin.svg"
+ inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns:inkstitch="http://inkstitch.org/namespace"><sodipodi:namedview
+ id="namedview5"
+ pagecolor="#ffffff"
+ bordercolor="#000000"
+ borderopacity="0.25"
+ inkscape:showpageshadow="2"
+ inkscape:pageopacity="0.0"
+ inkscape:pagecheckerboard="0"
+ inkscape:deskcolor="#d1d1d1"
+ inkscape:document-units="mm"
+ inkscape:zoom="1.0097187"
+ inkscape:cx="216.89209"
+ inkscape:cy="246.10815"
+ inkscape:window-width="1920"
+ inkscape:window-height="1131"
+ inkscape:window-x="0"
+ inkscape:window-y="32"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="g1"
+ showgrid="false" /><metadata
+ id="metadata3"><inkstitch:min_stitch_len_mm>0.3</inkstitch:min_stitch_len_mm><inkstitch:collapse_len_mm>3.0</inkstitch:collapse_len_mm><inkstitch:inkstitch_svg_version>3</inkstitch:inkstitch_svg_version></metadata><defs
+ id="defs1" />
+ <g
+ id="Layer_x0020_1"
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer">
+ <g
+ id="g6"
+ transform="matrix(3.72213,0,0,3.72213,-40581.951,-41363.2)"
+ style="opacity:1;stroke-width:0.268663"><path
+ class="fil1"
+ d="M 14667.311,12672.251 C 14670.674,12694.45 14689.847,12711.269 14712.719,12711.269 V 12711.269 C 14737.947,12711.269 14758.801,12690.75 14758.801,12665.186 V 12616.75 C 14758.801,12604.305 14764.856,12593.877 14775.283,12587.15 14786.047,12580.759 14798.492,12580.423 14809.256,12586.478 14837.174,12600.941 14855.674,12628.523 14855.674,12664.514 V 13602.296 C 14855.674,13651.741 14814.974,13692.441 14765.528,13692.441 H 14667.311 V 13841.45 C 14667.311,14399.478 14210.865,14855.923 13652.837,14855.923 H 12236.746 C 11678.719,14855.923 11221.937,14399.478 11221.937,13841.45 V 13706.232 H 11125.737 C 11074.946,13706.232 11033.573,13664.86 11033.573,13614.405 V 12679.986 C 11033.573,12629.532 11074.946,12588.159 11125.737,12588.159 H 11221.937 V 12425.359 C 11221.937,11867.331 11678.719,11410.886 12236.746,11410.886 H 13652.837 C 14210.865,11410.886 14667.311,11867.331 14667.311,12425.359 Z M 11150.292,13239.359 H 11186.282 C 11200.746,13239.359 11213.528,13246.423 11221.937,13256.85 V 13314.032 C 11213.528,13324.796 11200.746,13331.523 11186.282,13331.523 H 11150.292 C 11125.065,13331.523 11104.546,13310.669 11104.546,13285.441 V 13285.441 C 11104.546,13260.214 11125.065,13239.359 11150.292,13239.359 Z M 11168.455,13478.177 C 11193.683,13478.177 11214.201,13498.696 11214.201,13523.923 11214.201,13549.486 11193.683,13570.005 11168.455,13570.005 11142.892,13570.005 11122.374,13549.486 11122.374,13523.923 11122.374,13498.696 11142.892,13478.177 11168.455,13478.177 Z M 14705.319,13239.359 H 14741.311 C 14766.538,13239.359 14787.392,13260.214 14787.392,13285.441 V 13285.441 C 14787.392,13310.669 14766.538,13331.523 14741.311,13331.523 H 14705.319 C 14689.51,13331.523 14675.719,13323.45 14667.311,13311.005 V 13259.877 C 14675.719,13247.769 14689.51,13239.359 14705.319,13239.359 Z M 14723.483,13478.514 C 14748.711,13478.514 14768.892,13498.696 14768.892,13523.923 14768.892,13549.15 14748.711,13569.669 14723.483,13569.669 14698.256,13569.669 14677.738,13549.15 14677.738,13523.923 14677.738,13498.696 14698.256,13478.514 14723.483,13478.514 Z M 12236.746,11583.104 C 11773.91,11583.104 11394.492,11962.523 11394.492,12425.359 V 13841.45 C 11394.492,14304.287 11773.91,14683.706 12236.746,14683.706 H 13652.837 C 14115.674,14683.706 14495.093,14304.287 14495.093,13841.45 V 12425.359 C 14495.093,11962.523 14115.674,11583.104 13652.837,11583.104 Z"
+ id="path1"
+ style="fill:#003399;stroke:none;stroke-width:10.7404;stroke-dasharray:none"
+ sodipodi:nodetypes="cssssccssscsssscsssscsssscssccssssssssssssssssccsssssssssssssss" /></g>
+ <g
+ id="g1"
+ transform="matrix(0.951014 0 0 0.951014 1114.35 1312.15)"
+ style="stroke-width:1.05151"><path
+ id="path2"
+ style="fill:#bcbcbc;fill-opacity:1;stroke-width:120.295;stroke-linejoin:round"
+ sodipodi:type="inkscape:offset"
+ inkscape:radius="521.09528"
+ inkscape:original="M 8464.0547 1831.6582 L 4997.5117 1861.8086 C 4856.1909 1863.0871 4721.1611 1920.4298 4622.1055 2021.2324 L 2192.3125 4493.7891 C 2093.1704 4594.6337 2038.1605 4730.7389 2039.3906 4872.1504 L 2069.541 8338.498 C 2070.7673 8479.8872 2128.116 8614.9977 2228.9668 8714.1016 L 4701.5215 11143.895 C 4802.3983 11242.964 4938.5013 11297.899 5079.8848 11296.617 L 8546.2305 11266.467 C 8687.6198 11265.241 8822.7302 11207.893 8921.834 11107.043 L 11351.627 8634.4863 C 11450.648 8533.658 11505.583 8397.6378 11504.352 8256.3223 L 11474.201 4789.7773 C 11472.922 4648.4566 11415.577 4513.4285 11314.775 4414.373 L 8842.2207 1984.5801 C 8741.4246 1885.4864 8605.3984 1830.4805 8464.0547 1831.6582 z M 8252.6055 2899.5449 L 10410.057 5019.9492 L 10436.465 8044.873 L 8316.0605 10202.521 L 5291.332 10228.73 L 3133.6836 8108.5254 L 3107.2773 5083.5996 L 5227.6816 2925.9512 L 8252.6055 2899.5449 z "
+ d="M 8459.7129,1310.5801 A 521.14739,521.14739 0 0 0 8459.5234,1310.582 L 4992.9805,1340.7324 A 521.14739,521.14739 0 0 0 4992.7969,1340.7344 C 4713.5353,1343.2608 4446.1739,1456.8016 4250.4297,1655.998 L 1820.7188,4128.4687 C 1820.7107,4128.477 1820.7034,4128.4859 1820.6953,4128.4941 L 1820.6426,4128.5469 C 1624.778,4327.8162 1515.884,4597.2763 1518.3145,4876.6836 L 1548.4648,8343.0313 C 1550.8917,8622.4251 1664.4424,8889.9381 1863.7246,9085.7715 L 1863.7266,9085.7734 C 1863.7279,9085.7747 1863.7292,9085.7761 1863.7305,9085.7773 L 4336.2793,11515.564 A 521.14739,521.14739 0 0 0 4336.3984,11515.682 C 4535.7438,11711.455 4805.2284,11820.225 5084.6094,11817.691 L 8550.748,11787.543 C 8830.1468,11785.12 9097.6666,11671.571 9293.5039,11472.285 L 9293.5059,11472.283 11723.297,8999.7285 A 521.14739,521.14739 0 0 0 11723.414,8999.6094 C 11919.083,8800.3688 12027.858,8531.0479 12025.428,8251.791 V 8251.7891 L 11995.277,4785.2461 A 521.14739,521.14739 0 0 0 11995.275,4785.0605 C 11992.748,4505.7972 11879.204,4238.4418 11680.018,4042.7031 L 11680.016,4042.7012 9207.5391,1612.9863 C 9008.3554,1417.1668 8739.0223,1308.2528 8459.7129,1310.5801 Z M 8041.2969,3422.5059 9890.8652,5240.3125 9913.5059,7833.5781 8095.6973,9683.3164 5502.627,9705.7852 3652.875,7888.1328 3630.2363,5294.8945 5448.0605,3445.1426 Z" /><path
+ style="opacity:0.5;fill:none;fill-rule:evenodd;stroke:#53201c;stroke-width:58.7849;stroke-linejoin:bevel"
+ d="M 6696.4014,3616.0713 6779.6649,951.63941 M 9610.6236,6405.3984 H 12108.528 M 6696.4012,9361.2526 V 12067.316 M 3907.074,6738.4524 H 1117.7469 M 2824.6486,2408.7506 5010.3154,4594.4174 M 10693.049,2658.5411 8736.3568,4448.7062 M 10776.313,10402.046 8673.9092,8299.643 M 2783.0168,10443.678 4822.9725,8403.7223"
+ id="path3" /><path
+ d="M 1534.51 6738.45 L 1518.31 4876.68 L 1522.57 4772.47 L 1536.98 4669.94 L 1561.26 4569.73 L 1595.12 4472.54 L 1638.27 4379.13 L 1690.43 4290.23 L 1751.32 4206.45 L 1820.64 4128.55 L 1820.69 4128.47 L 1820.72 4128.47 L 3170.65 2754.76 L 4250.41 1655.99 L 4327.08 1585.36 L 4409.73 1523.05 L 4497.71 1469.37 L 4590.28 1424.61 L 4686.79 1389.07 L 4786.5 1363.04 L 4888.74 1346.83 L 4992.78 1340.73 L 4992.99 1340.73 L 6767.99 1325.29 L 8459.51 1310.58 L 8459.72 1310.58 L 8563.85 1314.87 L 8666.37 1329.31 L 8766.55 1353.61 L 8863.65 1387.48 L 8957.02 1430.63 L 9045.92 1482.79 L 9129.66 1543.67 L 9207.51 1612.98 L 10474.7 2858.28 L 11680 4042.71 L 11680 4042.71 L 11750.7 4119.35 L 11813 4201.99 L 11866.6 4289.97 L 11911.4 4382.54 L 11946.9 4479.05 L 11972.9 4578.77 L 11989.2 4681 L 11995.3 4785.04 L 11995.3 4785.25 L 12009.4 6405.39 L 12025.4 8251.77 L 12025.4 8251.77 L 12021.2 8355.94 L 12006.8 8458.43 L 11982.5 8558.6 L 11948.7 8655.71 L 11905.6 8749.07 L 11853.5 8837.98 L 11792.7 8921.71 L 11723.4 8999.61 L 11723.3 8999.73 L 10558.9 10184.6 L 9293.48 11472.3 L 9293.48 11472.3 L 9216.8 11542.9 L 9134.12 11605.3 L 9046.09 11659 L 8953.44 11703.7 L 8856.89 11739.3 L 8757.13 11765.3 L 8654.86 11781.5 L 8550.73 11787.5 L 6696.4 11803.7 L 5084.59 11817.7 L 4980.42 11813.5 L 4877.85 11799.1 L 4777.63 11774.8 L 4680.49 11741 L 4587.08 11698 L 4498.13 11645.8 L 4414.31 11585 L 4336.37 11515.7 L 4336.29 11515.6 L 3012.25 10214.4 L 1863.73 9085.78 L 1863.72 9085.78 L 1863.72 9085.74 L 1793.06 9009.07 L 1730.73 8926.38 L 1677.04 8838.36 L 1632.27 8745.75 L 1596.73 8649.19 L 1570.72 8549.39 L 1554.53 8447.12 L 1548.46 8343.04 L 1534.51 6738.45 M 3642.85 6738.45 L 3630.24 5294.91 L 4661.44 4245.54 L 5448.04 3445.12 L 6702.08 3434.19 L 8041.29 3422.5 L 8917.12 4283.29 L 9890.86 5240.3 L 9901.04 6405.39 L 9913.48 7833.55 L 9068.08 8693.83 L 8095.68 9683.29 L 6696.4 9695.44 L 5502.6 9705.78 L 4503.09 8723.59 L 3652.86 7888.11 L 3642.85 6738.45 M 6696.4 3616.08 L 6779.68 951.638 M 9610.61 6405.39 L 12108.5 6405.39 M 6696.4 9361.25 L 6696.4 12067.3 M 3907.05 6738.45 L 1117.75 6738.45 M 2824.65 2408.75 L 5010.31 4594.4 M 10693.1 2658.54 L 8736.37 4448.7 M 10776.3 10402 L 8673.9 8299.65 M 2783.01 10443.7 L 4822.95 8403.69"
+ style="stroke:#ff0000;fill:none;stroke-width:158.87762839;stroke-dasharray:none;stroke-opacity:1"
+ inkstitch:satin_column="True"
+ inkscape:label="Satin 1"
+ id="path4" /></g></g>
+</svg>
diff --git a/lib/extensions/__init__.py b/lib/extensions/__init__.py
index beafcc56..94e0b4bb 100644
--- a/lib/extensions/__init__.py
+++ b/lib/extensions/__init__.py
@@ -19,6 +19,7 @@ from .density_map import DensityMap
from .display_stacking_order import DisplayStackingOrder
from .duplicate_params import DuplicateParams
from .element_info import ElementInfo
+from .fill_to_satin import FillToSatin
from .fill_to_stroke import FillToStroke
from .flip import Flip
from .generate_palette import GeneratePalette
@@ -33,10 +34,10 @@ from .layer_commands import LayerCommands
from .lettering import Lettering
from .lettering_along_path import LetteringAlongPath
from .lettering_custom_font_dir import LetteringCustomFontDir
+from .lettering_edit_json import LetteringEditJson
from .lettering_font_sample import LetteringFontSample
from .lettering_force_lock_stitches import LetteringForceLockStitches
from .lettering_generate_json import LetteringGenerateJson
-from .lettering_edit_json import LetteringEditJson
from .lettering_remove_kerning import LetteringRemoveKerning
from .lettering_set_color_sort_index import LetteringSetColorSortIndex
from .letters_to_font import LettersToFont
@@ -88,6 +89,7 @@ __all__ = extensions = [About,
DisplayStackingOrder,
DuplicateParams,
ElementInfo,
+ FillToSatin,
FillToStroke,
Flip,
GeneratePalette,
diff --git a/lib/extensions/fill_to_satin.py b/lib/extensions/fill_to_satin.py
new file mode 100644
index 00000000..4a9f2904
--- /dev/null
+++ b/lib/extensions/fill_to_satin.py
@@ -0,0 +1,405 @@
+# Authors: see git history
+#
+# Copyright (c) 2025 Authors
+# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
+
+from collections import defaultdict
+
+from inkex import Boolean, Group, Path, PathElement
+from shapely.geometry import LineString, MultiLineString, Point
+from shapely.ops import linemerge, snap, split, substring
+
+from ..elements import FillStitch, Stroke
+from ..gui.abort_message import AbortMessageApp
+from ..i18n import _
+from ..svg import get_correction_transform
+from ..utils import ensure_multi_line_string
+from .base import InkstitchExtension
+
+
+class FillToSatin(InkstitchExtension):
+ def __init__(self, *args, **kwargs):
+ InkstitchExtension.__init__(self, *args, **kwargs)
+ self.arg_parser.add_argument("--notebook")
+ self.arg_parser.add_argument("--skip_end_section", dest="skip_end_section", type=Boolean, default=False)
+ self.arg_parser.add_argument("--center", dest="center", type=Boolean, default=False)
+ self.arg_parser.add_argument("--contour", dest="contour", type=Boolean, default=False)
+ 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
+
+ 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:
+ self.print_error()
+ return
+
+ 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)
+
+ 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._insert_satins(fill_element, combined_satins)
+
+ self._remove_originals()
+
+ def _insert_satins(self, fill_element, combined_satins):
+ '''Insert satin elements into the document'''
+ if not combined_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:
+ new_group = Group()
+ group.insert(index, new_group)
+ group = new_group
+ group.label = _("Satin Group")
+ index = 0
+ for i, satin in enumerate(combined_satins):
+ node = PathElement()
+ d = ""
+ for segment in satin:
+ for geom in segment.geoms:
+ d += str(Path(list(geom.coords)))
+ node.set('d', d)
+ node.set('style', style)
+ node.set('inkstitch:satin_column', True)
+ if self.options.center:
+ node.set('inkstitch:center_walk_underlay', True)
+ if self.options.contour:
+ node.set('inkstitch:contour_underlay', True)
+ if self.options.zigzag:
+ node.set('inkstitch:zigzag_underlay', True)
+ node.transform = transform
+ node.apply_transform()
+ node.label = _("Satin") + f" {self.satin_index}"
+ group.insert(index, node)
+ self.satin_index += 1
+
+ def _remove_originals(self):
+ '''Remove original elements - if requested'''
+ if not self.options.keep_originals:
+ for element in self.elements:
+ element.node.getparent().remove(element.node)
+
+ 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)
+ combined_rungs = defaultdict(list)
+
+ combined[0] = [0, 1]
+ combined_rungs[0] = [0, 1]
+
+ return self._combined_segments_to_satin_geoms(combined, combined_rungs, satin_segments)
+
+ def _get_satin_geoms(self, rung_segments, satin_segments):
+ '''Combine segments and return satin geometries'''
+ self.rung_segments = {rung: segments for rung, segments in rung_segments.items() if len(segments) == 2}
+ finished_rungs = []
+ finished_segments = []
+ combined_rails = defaultdict(list)
+ combined_rungs = defaultdict(list)
+
+ for rung, segments in self.rung_segments.items():
+ self._find_connected(rung, segments, rung, finished_rungs, finished_segments, combined_rails, combined_rungs)
+
+ unfinished = {i for i, segment in enumerate(satin_segments) if i not in finished_segments}
+ segment_count = len(satin_segments)
+ for i, segment in enumerate(unfinished):
+ index = segment_count + i + 1
+ combined_rails[index] = [segment]
+
+ return self._combined_segments_to_satin_geoms(combined_rails, combined_rungs, satin_segments)
+
+ def _combined_segments_to_satin_geoms(self, combined_rails, combined_rungs, satin_segments):
+ combined_satins = []
+ for i, segments in combined_rails.items():
+ segment_geoms = []
+ for segment_index in set(segments):
+ segment_geoms.extend(list(satin_segments[segment_index].geoms))
+ satin_rails = ensure_multi_line_string(linemerge(segment_geoms))
+ satin_rails = [self._adjust_rail_direction(satin_rails)]
+
+ segment_geoms = []
+ for rung_index in set(combined_rungs[i]):
+ rung = self.rungs[rung_index]
+ # satin behaves bad if a rung is positioned directly at the beginning/end section
+ if rung.distance(Point(satin_rails[0].geoms[0].coords[0])) > 1:
+ segment_geoms.append(ensure_multi_line_string(rung))
+ combined_satins.append(satin_rails + segment_geoms)
+ return combined_satins
+
+ def _get_segments(self, intersection_points): # noqa: C901
+ '''Combine line sections to satin segments (find the rails that belong together)'''
+ line_section_multi = MultiLineString(self.line_sections)
+ rung_segments = defaultdict(list)
+ satin_segments = []
+
+ segment_index = 0
+ finished_sections = []
+ for i, section in enumerate(self.line_sections):
+ if i in finished_sections:
+ continue
+ s_rungs = self.section_rungs[i]
+ if len(s_rungs) == 1:
+ if self.options.skip_end_section and len(self.rungs) > 1:
+ continue
+ segment = self._get_end_segment(section)
+ satin_segments.append(segment)
+ finished_sections.append(i)
+ for rung in s_rungs:
+ rung_segments[rung].append(segment_index)
+ segment_index += 1
+
+ elif len(s_rungs) == 2:
+ connected_section = self._get_connected_section(i, s_rungs)
+ if connected_section:
+ connect_index, segment = self._get_standard_segment(connected_section, s_rungs, section, finished_sections)
+ if segment is None:
+ continue
+ satin_segments.append(segment)
+ for rung in s_rungs:
+ 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
+ return rung_segments, satin_segments
+
+ def _get_end_segment(self, section):
+ section = section.simplify(0.5)
+ rail1 = substring(section, 0, 0.40009, True).coords
+ rail2 = substring(section, 0.50001, 1, True).coords
+ if len(rail1) > 2:
+ rail1 = rail1[:-1]
+ if len(rail2) > 2:
+ rail2 = rail2[1:]
+
+ segment = MultiLineString([LineString(rail1), LineString(rail2)])
+ return segment
+
+ def _get_standard_segment(self, connected_section, s_rungs, section, finished_sections):
+ section2 = None
+ segment = None
+ connect_index = None
+ if len(connected_section) == 1:
+ section2 = self.line_sections[connected_section[0]]
+ connect_index = connected_section[0]
+ else:
+ for connect in connected_section:
+ if connect in finished_sections:
+ continue
+ offset_rung = self.rungs[s_rungs[0]].offset_curve(0.01)
+ section_candidate = self.line_sections[connect]
+ if offset_rung.intersects(section) == offset_rung.intersects(section_candidate):
+ section2 = section_candidate
+ connect_index = connect
+ break
+ if section2 is not None:
+ 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:
+ connections = self.rung_sections[rung]
+ rung_section_list.append(connections)
+ connected_section = list(set(rung_section_list[0]) & set(rung_section_list[1]))
+ connected_section.remove(index)
+ return connected_section
+
+ def _adjust_rail_direction(self, satin_rails):
+ # See also elements/satin_column.py (_get_rails_to_reverse)
+ rails = list(satin_rails.geoms)
+ lengths = []
+ lengths_reverse = []
+
+ for i in range(10):
+ distance = i / 10
+ point0 = rails[0].interpolate(distance, normalized=True)
+ point1 = rails[1].interpolate(distance, normalized=True)
+ point1_reverse = rails[1].interpolate(1 - distance, normalized=True)
+
+ lengths.append(point0.distance(point1))
+ lengths_reverse.append(point0.distance(point1_reverse))
+
+ if sum(lengths) > sum(lengths_reverse):
+ rails[0] = rails[0].reverse()
+
+ return MultiLineString(rails)
+
+ def _find_connected(self, rung, segments, first_rung, finished_rungs, finished_segments, combined_rails, combined_rungs):
+ '''Group combinable segments'''
+ if rung in finished_rungs:
+ return
+ finished_rungs.append(rung)
+ combined_rails[first_rung].extend(segments)
+ combined_rungs[first_rung].append(rung)
+ finished_segments.extend(segments)
+ for segment in segments:
+ connected = self._get_combinable_segments(segment, segments)
+ if not connected:
+ continue
+ for connected_rung, connected_segments in connected.items():
+ self._find_connected(
+ connected_rung,
+ connected_segments,
+ first_rung, finished_rungs,
+ finished_segments,
+ combined_rails,
+ combined_rungs
+ )
+
+ def _get_combinable_segments(self, segment, segments_in):
+ '''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):
+ '''Splits the fill outline into sections. Splitter is a MultiLineString with all available rungs'''
+ rungs = MultiLineString(self.rungs)
+ for line in fill_linestrings:
+ sections = list(ensure_multi_line_string(split(line, rungs)).geoms)
+ if len(sections) > 1:
+ # merge end and start section
+ sections[0] = linemerge(MultiLineString([sections[0], sections[-1]]))
+ del sections[-1]
+ self.line_sections.extend(sections)
+
+ def _define_relations(self, bridges):
+ ''' 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
+ '''
+ 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)
+
+ def _validate_rungs(self, linestrings):
+ ''' Returns only valid rungs and bridge section markers'''
+ multi_line_string = MultiLineString(linestrings)
+ valid_rungs = []
+ bridge_indicators = []
+ intersection_points = []
+ 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)
+ 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()
diff --git a/lib/extensions/fill_to_stroke.py b/lib/extensions/fill_to_stroke.py
index db5719a6..9101eca2 100644
--- a/lib/extensions/fill_to_stroke.py
+++ b/lib/extensions/fill_to_stroke.py
@@ -227,7 +227,7 @@ class FillToStroke(InkstitchExtension):
continue
def _close_gaps(self, lines, cut_lines):
- snaped_lines = []
+ snapped_lines = []
lines = MultiLineString(lines)
for i, line in enumerate(lines.geoms):
# for each cutline check if a line starts or ends close to it
@@ -239,16 +239,16 @@ class FillToStroke(InkstitchExtension):
l_l = lines.difference(line)
for cut_line in cut_lines:
distance = start.distance(l_l)
- if cut_line.distance(start) < 0.6:
+ if cut_line.distance(start) < 1:
distance = start.distance(l_l)
new_start_point = self._extend_line(line.coords[0], line.coords[1], distance)
coords[0] = nearest_points(Point(list(new_start_point)), l_l)[1]
- if cut_line.distance(end) < 0.6:
+ if cut_line.distance(end) < 1:
distance = end.distance(l_l)
new_end_point = self._extend_line(line.coords[-1], line.coords[-2], distance)
coords[-1] = nearest_points(Point(list(new_end_point)), l_l)[1]
- snaped_lines.append(LineString(coords))
- return snaped_lines
+ snapped_lines.append(LineString(coords))
+ return snapped_lines
def _extend_line(self, p1, p2, distance):
start_point = InkstitchPoint.from_tuple(p1)
diff --git a/lib/lettering/font.py b/lib/lettering/font.py
index 6127a84c..93e550d8 100644
--- a/lib/lettering/font.py
+++ b/lib/lettering/font.py
@@ -577,7 +577,7 @@ class Font(object):
group.append(color_group)
- def _get_color_sorted_elements(self, group, transform_key): # noqa: 901
+ def _get_color_sorted_elements(self, group, transform_key): # noqa: C901
elements_by_color = defaultdict(list)
last_parent = None
diff --git a/templates/fill_to_satin.xml b/templates/fill_to_satin.xml
new file mode 100644
index 00000000..5547bfcd
--- /dev/null
+++ b/templates/fill_to_satin.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<inkscape-extension translationdomain="inkstitch" xmlns="http://www.inkscape.org/namespace/inkscape/extension">
+ <name>Fill to satin</name>
+ <id>org.{{ id_inkstitch }}.fill_to_satin</id>
+ <param name="extension" type="string" gui-hidden="true">fill_to_satin</param>
+
+ <param name="notebook" type="notebook">
+ <page name="options" gui-text="Options">
+ <param name="skip_end_section" type="boolean" gui-text="Start / end at rung"
+ gui-description="Needs at least 2 rungs">false</param>
+ <spacer />
+ <separator />
+ <spacer />
+ <param name="center" type="boolean" gui-text="Center-walk underlay">false</param>
+ <param name="contour" type="boolean" gui-text="Contour underlay">false</param>
+ <param name="zigzag" type="boolean" gui-text="Zig-zag underlay">false</param>
+ <spacer />
+ <separator />
+ <spacer />
+ <param name="keep_originals" type="boolean" gui-text="Keep original paths">false</param>
+ </page>
+ <page name="info" gui-text="Help">
+ <label>This extension takes in a fill and rung elements and converts them into satin(s).</label>
+ <spacer />
+ <label>More information on our website</label>
+ <label appearance="url">https://inkstitch.org/docs/satin-tools/#fill-to-satin</label>
+ </page>
+ </param>
+
+ <effect>
+ <object-type>all</object-type>
+ <icon>{{ icon_path }}inx/fill_to_satin.svg</icon>
+ <menu-tip>Convert fill elements to satin</menu-tip>
+ <effects-menu>
+ <submenu name="{{ menu_inkstitch }}" translatable="no">
+ <submenu name="Tools: Satin" />
+ </submenu>
+ </effects-menu>
+ </effect>
+ <script>
+ {{ command_tag | safe }}
+ </script>
+</inkscape-extension>