diff options
| -rw-r--r-- | lib/elements/fill_stitch.py | 2 | ||||
| -rw-r--r-- | lib/extensions/__init__.py | 2 | ||||
| -rw-r--r-- | lib/extensions/jump_to_stroke.py | 2 | ||||
| -rw-r--r-- | lib/extensions/transform_elements.py | 112 | ||||
| -rw-r--r-- | templates/transform_elements.xml | 38 |
5 files changed, 154 insertions, 2 deletions
diff --git a/lib/elements/fill_stitch.py b/lib/elements/fill_stitch.py index 5f33eb65..fa4ce845 100644 --- a/lib/elements/fill_stitch.py +++ b/lib/elements/fill_stitch.py @@ -311,7 +311,7 @@ class FillStitch(EmbroideryElement): type='float', sort_index=21, select_items=[('fill_method', 'tartan_fill')], - default=45) + default=-45) @cache def tartan_angle(self): return self.get_float_param('tartan_angle', -45) diff --git a/lib/extensions/__init__.py b/lib/extensions/__init__.py index 60d0e10d..22da70a7 100644 --- a/lib/extensions/__init__.py +++ b/lib/extensions/__init__.py @@ -72,6 +72,7 @@ from .stroke_to_satin import StrokeToSatin from .tartan import Tartan from .test_swatches import TestSwatches from .thread_list import ThreadList +from .transform_elements import TransformElements from .troubleshoot import Troubleshoot from .unlink_clone import UnlinkClone from .update_svg import UpdateSvg @@ -148,6 +149,7 @@ extensions = [ Tartan, TestSwatches, ThreadList, + TransformElements, Troubleshoot, UnlinkClone, UpdateSvg, diff --git a/lib/extensions/jump_to_stroke.py b/lib/extensions/jump_to_stroke.py index 867b859f..7470faeb 100644 --- a/lib/extensions/jump_to_stroke.py +++ b/lib/extensions/jump_to_stroke.py @@ -49,7 +49,7 @@ class JumpToStroke(InkstitchExtension): next_elements = self.elements[1:] + next_elements for element, next_element in zip(self.elements, next_elements): layer, group = self._get_element_layer_and_group(element) - stitch_groups = element.to_stitch_groups(last_stitch_group, next_element) + stitch_groups = element.embroider(last_stitch_group, next_element) multiple = not self.options.merge_subpaths and stitch_groups if multiple: diff --git a/lib/extensions/transform_elements.py b/lib/extensions/transform_elements.py new file mode 100644 index 00000000..66d0ad82 --- /dev/null +++ b/lib/extensions/transform_elements.py @@ -0,0 +1,112 @@ +# Authors: see git history +# +# Copyright (c) 2025 Authors +# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. + +import json +from typing import Optional + +from inkex import (Boolean, PathElement, ShapeElement, Transform, Vector2d, + errormsg) + +from ..i18n import _ +from ..tartan.utils import get_tartan_settings +from .base import InkstitchExtension + + +class TransformElements(InkstitchExtension): + ''' + This will apply transformations while also transforming fill angles. + ''' + def __init__(self, *args, **kwargs): + InkstitchExtension.__init__(self, *args, **kwargs) + self.arg_parser.add_argument("--notebook") + self.arg_parser.add_argument("-r", "--rotate", type=float, default=0, dest="rotate") + self.arg_parser.add_argument("-f", "--flip-horizontally", type=Boolean, default=False, dest="horizontal_flip") + self.arg_parser.add_argument("-v", "--flip-vertically", type=Boolean, default=False, dest="vertical_flip") + + def effect(self) -> None: + if not self.svg.selection: + errormsg(_("Please select one or more elements.")) + return + selection_center = self.svg.selection.bounding_box().center + + nodes = self.get_nodes() + for node in nodes: + parent_transform = node.composed_transform() @ -node.transform + if self.options.rotate != 0: + self.rotate_node(node, parent_transform, selection_center) + if self.options.horizontal_flip: + self.flip_node_horizontally(node, parent_transform, selection_center) + if self.options.vertical_flip: + self.flip_node_vertically(node, parent_transform, selection_center) + + # Apply transform to path elements, simply because it's possible and nicer + if isinstance(node, PathElement): + node.apply_transform() + + def flip_node_vertically(self, node: ShapeElement, parent_transform: Transform, center: Vector2d) -> None: + node.transform = ( + -parent_transform @ + Transform(f'translate({center[0], center[1]}) scale(1, -1) translate({-center[0], -center[1]})') @ + node.composed_transform() + ) + self.adapt_fill_angle(node, -1) + + def flip_node_horizontally(self, node: ShapeElement, parent_transform: Transform, center: Vector2d) -> None: + node.transform = ( + -parent_transform @ + Transform(f'translate({center[0], center[1]}) scale(-1, 1) translate({-center[0], -center[1]})') @ + node.composed_transform() + ) + self.adapt_fill_angle(node, -1) + + def rotate_node(self, node: ShapeElement, parent_transform: Transform, center: Vector2d) -> None: + node.transform = ( + -parent_transform @ + Transform(f'rotate({self.options.rotate}, {center[0]}, {center[1]})') @ + node.composed_transform() + ) + self.adapt_fill_angle(node, None, self.options.rotate) + + def adapt_fill_angle(self, node: ShapeElement, multiplier: Optional[int] = None, rotation: Optional[float] = None) -> None: + if not node.style("fill", "black"): + return + + self._apply_angle(node, "inkstitch:fill_underlay_angle", None, multiplier, rotation) + if node.get('inkstitch:fill_method', None) == "tartan_fill": + if rotation is None: + self._apply_angle(node, "inkstitch:tartan_angle", "-45", multiplier, rotation) + # Also rotate tartan pattern rotation setting + self._rotate_tartan_pattern(node, multiplier, rotation) + elif node.get('inkstitch:fill_method', None) == "meander_fill": + self._apply_angle(node, "inkstitch:meander_angle", "0", multiplier, rotation) + else: + self._apply_angle(node, "inkstitch:angle", "0", multiplier, rotation) + + def _apply_angle(self, node: ShapeElement, attrib: str, default: Optional[str], multiplier: Optional[int], rotation: Optional[float]) -> None: + angle_string = node.get(attrib, default) + if angle_string is None: + return + + try: + angle = float(angle_string) + except ValueError: + return + + if multiplier is not None: + angle *= multiplier + elif rotation is not None: + angle -= rotation + node.set(attrib, str(angle)) + + def _rotate_tartan_pattern(self, node: ShapeElement, multiplier: Optional[int], rotation: Optional[float]) -> None: + settings = get_tartan_settings(node) + tartan_rotation = settings['rotate'] + + if multiplier is not None: + tartan_rotation *= multiplier + elif rotation is not None: + tartan_rotation += rotation + settings['rotate'] = tartan_rotation + node.set("inkstitch:tartan", json.dumps(settings)) diff --git a/templates/transform_elements.xml b/templates/transform_elements.xml new file mode 100644 index 00000000..5bc43cdd --- /dev/null +++ b/templates/transform_elements.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="UTF-8"?> +<inkscape-extension translationdomain="inkstitch" xmlns="http://www.inkscape.org/namespace/inkscape/extension"> + <name>Transform</name> + <id>org.{{ id_inkstitch }}.transform_elements</id> + <param name="extension" type="string" gui-hidden="true">transform_elements</param> + + <param name="notebook" type="notebook"> + <page name="options" gui-text="Options"> + <param name="rotate" type="float" precision="2" min="-180" max="180" appearance="full" + gui-text="Rotate">0</param> + <param name="flip-horizontally" type="bool" gui-text="Flip horizontally">false</param> + <param name="flip-vertically" type="bool" gui-text="Flip vertically">false</param> + </page> + <page name="info" gui-text="Help"> + <label> + This extension applies transformations while also adjusting the fill angles + </label> + <spacer /> + <label>More information on our website</label> + <label appearance="url">https://inkstitch.org/docs/edit/#transform</label> + </page> + </param> + + <effect> + <object-type>all</object-type> + <icon>{{ icon_path }}inx/break_apart_fill.svg</icon> + <menu-tip>Break apart and repair (broken) fill shapes</menu-tip> + <effects-menu> + <submenu name="{{ menu_inkstitch }}" translatable="no"> + <submenu name="Edit" /> + </submenu> + </effects-menu> + </effect> + + <script> + {{ command_tag | safe }} + </script> +</inkscape-extension> |
