summaryrefslogtreecommitdiff
path: root/lib/extensions/zigzag_line_to_satin.py
blob: 8794f4dcbdca31cc494a4ceb8670caae0a0f7c2b (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
# 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):
        if not self.svg.selection or not self.get_elements():
            inkex.errormsg(_("Please select at least one stroke to convert to a satin column."))
            return

        for node in self.svg.selection:
            d = []
            point_list = list(node.get_path().end_points)
            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)
                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_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):
        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 handle positions
                    start = inkex.DirectedLineSegment(prev_prev, point)
                    end = inkex.DirectedLineSegment(next, prev)
                    if not start.length == 0:
                        start = start.parallel(*prev).point_at_length(start.length - length)
                    else:
                        start = start.start
                    if not end.length == 0:
                        end = end.parallel(*point).point_at_length(end.length - length)
                    else:
                        end = end.start

                    # generate curves
                    path_commands.append(inkex.paths.Curve(*start, *end, *point))

                    if self.options.pattern == 'zigzag' and (has_equal_rail_point_count or 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])
                        point0 = line.point_at_length(-50)
                        point1 = line.point_at_length(line.length + 50)
                        new_point = inkex.bezier.linebezierintersect((point0, point1), [prev, start, 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 _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