summaryrefslogtreecommitdiff
path: root/lib/extensions/transform_elements.py
blob: 66d0ad82e6e56fc90b32a63c83deac8a4adfa34f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
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))