summaryrefslogtreecommitdiff
path: root/lib/extensions/zigzag_line_to_satin.py
blob: 4f61f1ed3e5dba501fcae4dbe54451571dbc11c7 (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
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
# Authors: see git history
#
# Copyright (c) 2010 Authors
# Licensed under the GNU GPL version 3.0 or later.  See the file LICENSE for details.

import inkex

from ..i18n import _
from .base import InkstitchExtension


class ZigzagLineToSatin(InkstitchExtension):
    """Convert a satin column into a running stitch."""
    def __init__(self, *args, **kwargs):
        InkstitchExtension.__init__(self, *args, **kwargs)
        self.arg_parser.add_argument("--zigzag_to_satin", type=str, default=None)
        self.arg_parser.add_argument("--options", type=str, default=None)
        self.arg_parser.add_argument("--info", type=str, default=None)

        self.arg_parser.add_argument("-s", "--smoothing", type=inkex.Boolean, default=True, dest="smoothing")
        self.arg_parser.add_argument("-p", "--pattern", type=str, default="square", dest="pattern")
        self.arg_parser.add_argument("-r", "--rungs", type=inkex.Boolean, default=True, dest="rungs")
        self.arg_parser.add_argument("-l", "--reduce-rungs", type=inkex.Boolean, default=False, dest="reduce_rungs")

    def effect(self):
        nodes = self.get_selection(self.svg.selection)
        if not nodes:
            inkex.errormsg(_("Please select at least one stroke to convert to a satin column."))
            return

        for node in nodes:
            d = []
            point_list = list(node.get_path().end_points)
            # find duplicated nodes (= do not smooth)
            point_list, sharp_edges = self._get_sharp_edge_nodes(point_list)
            rails, rungs = self._get_rails_and_rungs(point_list)

            if not self.options.smoothing:
                for rail in rails:
                    d.append('M ' + ' '.join([str(point) for point in rail]))
            else:
                rails, rungs = self._smooth_path(rails, rungs, sharp_edges)
                d.append(rails)

            if self.options.rungs:
                if self.options.pattern != 'zigzag' and self.options.reduce_rungs and len(rungs) > 2:
                    rungs = rungs[0::2]
                d.extend(self._rung_path(rungs))

            node.set('d', " ".join(d))
            node.set('inkstitch:satin_column', True)

    def get_selection(self, nodes):
        selection = []
        for node in nodes:
            # we only apply to path elements, no use in converting ellipses or rectangles, etc.
            if node.TAG == "path":
                selection.append(node)
            elif node.TAG == "g":
                for element in node.descendants():
                    selection.extend(self.get_selection(element))
        return selection

    def _get_sharp_edge_nodes(self, point_list):
        points = []
        sharp_edges = []
        skip = False
        for p0, p1 in zip(point_list[:-1], point_list[1:]):
            if skip is True:
                skip = False
                continue
            points.append(p0)
            if inkex.DirectedLineSegment(p0, p1).length < 0.3:
                sharp_edges.append(p0)
                skip = True
        points.append(p1)
        return points, sharp_edges

    def _get_rails_and_rungs(self, point_list):
        if self.options.pattern == "sawtooth":
            # sawtooth pattern: |/|/|/|
            rails = [point_list[0::2], point_list[1::2]]
            rungs = list(zip(point_list[1::2], point_list[:-1:2]))
            return rails, rungs
        elif self.options.pattern == "zigzag":
            # zigzag pattern: VVVVV
            rails = [point_list[0::2], point_list[1::2]]
            rail_points = [[], []]
            rung_points = [[], []]
            for i, rail in enumerate(rails):
                for j, point in enumerate(rail):
                    if j == 0 or point in point_list[2::len(point_list)-3]:
                        rail_points[i].append(point)
                        rung_points[i].append(point)
                        continue
                    p0 = rail[j-1]
                    rail_points[i].append(point)
                    rung_points[i].append(inkex.Vector2d(inkex.DirectedLineSegment(p0, point).point_at_ratio(0.5)))
                    rung_points[i].append(point)
            rungs = list(zip(*rung_points))
            return rail_points, rungs
        else:
            # square pattern: |_|▔|_|▔|
            point_list = [point_list[i:i+4] for i in range(0, len(point_list), 4)]

            rungs = []
            rails = [[], []]
            for i, points in enumerate(point_list):
                if len(points) <= 1:
                    break

                elif len(points) < 4 and len(points) > 1:
                    rails[0].append(points[0])
                    rails[1].append(points[1])
                    rungs.append([points[0], points[1]])
                    break

                rails[0].extend([points[0], points[3]])
                rails[1].extend([points[1], points[2]])
                rungs.extend([[points[0], points[1]], [points[2], points[3]]])
            return rails, rungs

    def _smooth_path(self, rails, rungs, sharp_edges):  # noqa: C901
        path_commands = []
        new_rungs = []
        k = [1, 0]
        smoothing = 0.4
        has_equal_rail_point_count = len(rails[0]) == len(rails[1])
        for j, rail in enumerate(rails):
            r = rungs[j:len(rungs):2]
            for i, point in enumerate(rail):
                if i == 0:
                    path_commands.append(inkex.paths.Move(*point))
                else:
                    # get the two previous points and the next point for handle calculation
                    if i < 2:
                        prev_prev = rail[i - 1]
                    else:
                        prev_prev = rail[i-2]

                    prev = rail[i-1]

                    if i > len(rail) - 2:
                        next = point
                    else:
                        next = rail[i+1]

                    # get length of handles
                    length = inkex.DirectedLineSegment(point, prev).length * smoothing

                    # get start handle positions
                    start = inkex.DirectedLineSegment(prev_prev, point)
                    handle_position_start = self._get_handle_position(prev, start, sharp_edges, length)

                    # get end handle positions
                    end = inkex.DirectedLineSegment(next, prev)
                    handle_position_end = self._get_handle_position(point, end, sharp_edges, length)

                    # generate curves
                    path_commands.append(inkex.paths.Curve(*handle_position_start, *handle_position_end, *point))

                    # recalculate rungs for zigzag pattern
                    if self.options.pattern == 'zigzag' and i <= len(r) and (not self.options.reduce_rungs or j == 0):
                        # in zigzag mode we do have alternating points on rails
                        # when smoothing them out, rungs may not intersect anymore
                        # so we need to find a spot on the smoothed rail to ensure the correct length
                        rung = r[i-1]
                        line = inkex.DirectedLineSegment(rung[0], rung[1])
                        if line.length == 0:
                            continue

                        point0 = line.point_at_length(-50)
                        point1 = line.point_at_length(line.length + 50)
                        new_point = inkex.bezier.linebezierintersect((point0, point1), [prev, handle_position_start, handle_position_end, point])
                        if new_point:
                            new_rungs.append((rung[k[j]], new_point[0]))
                        else:
                            new_rungs.append(rung)

        rungs = self._update_rungs(new_rungs, rungs, r, has_equal_rail_point_count)
        return str(inkex.Path(path_commands)), rungs

    def _get_handle_position(self, point, segment, sharp_edges, length):
        if point in sharp_edges:
            handle_position = point
        elif segment.length > 0.0001:
            handle_position = segment.parallel(*point).point_at_length(segment.length - length)
        else:
            handle_position = segment.start
        return handle_position

    def _update_rungs(self, new_rungs, rungs, r, has_equal_rail_point_count):
        if self.options.pattern == 'zigzag':
            rungs = new_rungs
            if not has_equal_rail_point_count and not self.options.reduce_rungs:
                # make sure, that the last rail on canvas is also the last rail in the list
                # this important when we delete the very first and very last rung
                count = len(r)
                rungs[count], rungs[-1] = rungs[-1], rungs[count]
        return rungs

    def _rung_path(self, rungs):
        if len(rungs) < 3:
            return []
        d = []
        rungs = rungs[1:-1]
        for point0, point1 in rungs:
            line = inkex.DirectedLineSegment(point0, point1)
            point0 = line.point_at_length(-0.8)
            point1 = line.point_at_length(line.length + 0.8)
            d.append(f'M {point0[0]}, {point0[1]} {point1[0]}, {point1[1]}')
        return d