diff options
| author | Kaalleen <36401965+kaalleen@users.noreply.github.com> | 2025-01-05 12:52:02 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-01-05 12:52:02 +0100 |
| commit | 51ee6c7a08d559d43cd402febe6f9bd4b33c30b0 (patch) | |
| tree | b61bbf9f1ff686b917a1b2a0ee9b1a2223508580 | |
| parent | be66297da6aec234ef807b20609943f3c3252c2f (diff) | |
Fill to satin (#3406)
| -rw-r--r-- | icons/inx/fill_to_satin.svg | 69 | ||||
| -rw-r--r-- | lib/extensions/__init__.py | 4 | ||||
| -rw-r--r-- | lib/extensions/fill_to_satin.py | 405 | ||||
| -rw-r--r-- | lib/extensions/fill_to_stroke.py | 10 | ||||
| -rw-r--r-- | lib/lettering/font.py | 2 | ||||
| -rw-r--r-- | templates/fill_to_satin.xml | 43 |
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> |
