summaryrefslogtreecommitdiff
path: root/lib/extensions/jump_to_stroke.py
blob: 7470faebf9a44af5277d292ccac2e1cc6e12add0 (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
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
# Authors: see git history
#
# Copyright (c) 2023 Authors
# Licensed under the GNU GPL version 3.0 or later.  See the file LICENSE for details.

from inkex import Boolean, DirectedLineSegment, Path, PathElement, Transform

from ..elements import Stroke
from ..svg import PIXELS_PER_MM, generate_unique_id, get_correction_transform
from ..svg.tags import INKSTITCH_ATTRIBS, SVG_GROUP_TAG
from .base import InkstitchExtension


class JumpToStroke(InkstitchExtension):
    """Adds a running stitch as a connection between two (or more) selected elements.
       The elements must have the same color and a minimum distance (collapse_len)."""

    def __init__(self, *args, **kwargs):
        InkstitchExtension.__init__(self, *args, **kwargs)
        self.arg_parser.add_argument("--tab")

        self.arg_parser.add_argument("-i", "--minimum-jump-length", type=float, default=3.0, dest="min_jump")
        self.arg_parser.add_argument("-a", "--maximum-jump-length", type=float, default=0, dest="max_jump")
        self.arg_parser.add_argument("--connect", type=str, default="all", dest="connect")
        self.arg_parser.add_argument("--exclude-trim", type=Boolean, default=True, dest="exclude_trim")
        self.arg_parser.add_argument("--exclude-stop", type=Boolean, default=True, dest="exclude_stop")
        self.arg_parser.add_argument("--exclude-force-lock-stitch", type=Boolean, default=True, dest="exclude_forced_lock")

        self.arg_parser.add_argument("-m", "--merge", type=Boolean, default=False, dest="merge")
        self.arg_parser.add_argument("--merge_subpaths", type=Boolean, default=False, dest="merge_subpaths")
        self.arg_parser.add_argument("-l", "--stitch-length", type=float, default=2.5, dest="running_stitch_length_mm")
        self.arg_parser.add_argument("-t", "--tolerance", type=float, default=2.0, dest="running_stitch_tolerance_mm")

    def effect(self):
        self._set_selection()
        self.get_elements()

        if self.options.merge_subpaths:
            # when we merge stroke elements we are going to replace original path elements
            # which would be bad in the case that the element has more subpaths
            self._split_stroke_elements_with_subpaths()

        last_group = None
        last_layer = None
        last_element = None
        last_stitch_group = None
        next_elements = [None]
        if len(self.elements) > 1:
            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.embroider(last_stitch_group, next_element)
            multiple = not self.options.merge_subpaths and stitch_groups

            if multiple:
                ending_point = stitch_groups[0].stitches[0]
                stitch_groups = [stitch_groups[-1]]

            if (not stitch_groups or
                    last_element is None or
                    (self.options.connect == "layer" and last_layer != layer) or
                    (self.options.connect == "group" and last_group != group) or
                    (self.options.exclude_trim and (last_element.has_command("trim") or last_element.trim_after)) or
                    (self.options.exclude_stop and (last_element.has_command("stop") or last_element.stop_after)) or
                    (self.options.exclude_forced_lock and last_element.force_lock_stitches)):
                last_layer = layer
                last_group = group
                last_element = element
                if stitch_groups:
                    last_stitch_group = stitch_groups[-1]
                continue

            for stitch_group in stitch_groups:
                if last_stitch_group is None or stitch_group.color != last_stitch_group.color:
                    last_layer = layer
                    last_group = group
                    last_stitch_group = stitch_group
                    continue
                start = last_stitch_group.stitches[-1]
                if multiple:
                    end = ending_point
                else:
                    end = stitch_group.stitches[0]
                self.generate_stroke(last_element, element, start, end)
                last_stitch_group = stitch_group

            last_group = group
            last_layer = layer
            last_element = element

    def _set_selection(self):
        if not self.svg.selection:
            self.svg.selection.clear()

    def _get_element_layer_and_group(self, element):
        layer = None
        group = None
        for ancestor in element.node.iterancestors(SVG_GROUP_TAG):
            if group is None:
                group = ancestor
            if ancestor.groupmode == "layer":
                layer = ancestor
                break
        return layer, group

    def _split_stroke_elements_with_subpaths(self):
        elements = []
        for element in self.elements:
            if isinstance(element, Stroke) and len(element.paths) > 1:
                if element.get_param('stroke_method', None) in ['ripple_stitch']:
                    elements.append(element)
                    continue
                node = element.node
                parent = node.getparent()
                index = parent.index(node)
                paths = node.get_path().break_apart()

                block_ids = []
                for path in paths:
                    subpath_element = node.copy()
                    subpath_id = generate_unique_id(node, f'{node.get_id()}_', block_ids)
                    subpath_element.set('id', subpath_id)
                    subpath_element.set('d', str(path))
                    block_ids.append(subpath_id)
                    parent.insert(index, subpath_element)
                    elements.append(Stroke(subpath_element))
                node.delete()
            else:
                elements.append(element)
        self.elements = elements

    def _is_mergable(self, element1, element2):
        if not (isinstance(element1, Stroke)):
            return False
        if (self.options.merge_subpaths and
                element1.node.get_id() not in self.svg.selection.ids and
                element2.node.get_id() not in self.svg.selection.ids):
            return True
        if (self.options.merge and
                element1.node.TAG == "path" and
                element1.get_param('stroke_method', None) == element2.get_param('stroke_method', None) and
                not element1.get_param('stroke_method', '') == 'ripple_stitch'):
            return True
        return False

    def generate_stroke(self, last_element, element, start, end):
        node = element.node
        parent = node.getparent()
        index = parent.index(node)

        # do not add a running stitch if the distance is smaller than min_jump setting
        line = DirectedLineSegment((start.x, start.y), (end.x, end.y))
        if line.length < self.options.min_jump * PIXELS_PER_MM:
            return
        # do not add a running stitch if the distance is longer than max_jump setting
        if self.options.max_jump > 0 and line.length > self.options.max_jump * PIXELS_PER_MM:
            return

        path = Path([(start.x, start.y), (end.x, end.y)])
        # option: merge line with paths
        merged = False
        if self._is_mergable(last_element, element):
            path.transform(Transform(get_correction_transform(last_element.node, True)), True)
            path = last_element.node.get_path() + path[1:]
            last_element.node.set('d', str(path))
            path.transform(-Transform(get_correction_transform(last_element.node)), True)
            merged = True
        if self._is_mergable(element, last_element):
            path.transform(Transform(get_correction_transform(node)), True)
            path = path + node.get_path()[1:]
            node.set('d', str(path))
            if merged:
                # remove last element (since it is merged)
                last_parent = last_element.node.getparent()
                last_element.node.delete()
                # remove parent group if empty
                if len(last_parent) == 0:
                    last_parent.delete()
            return

        if merged:
            return

        # add simple stroke to connect elements
        path.transform(Transform(get_correction_transform(node)), True)
        color = element.color
        style = f'stroke:{color};stroke-width:{self.svg.viewport_to_unit("1px")};stroke-dasharray:3, 1;fill:none;'

        line = PathElement(d=str(path), style=style)
        line.set(INKSTITCH_ATTRIBS['running_stitch_length_mm'], self.options.running_stitch_length_mm)
        line.set(INKSTITCH_ATTRIBS['running_stitch_tolerance_mm'], self.options.running_stitch_tolerance_mm)
        parent.insert(index, line)


if __name__ == '__main__':
    JumpToStroke().run()