summaryrefslogtreecommitdiff
path: root/lib/extensions/cutwork_segmentation.py
blob: 537cfadfdfa693d33cdaec9c3af55b5cb39b600e (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
# Authors: see git history
#
# Copyright (c) 2022 Authors
# Licensed under the GNU GPL version 3.0 or later.  See the file LICENSE for details.

from math import atan2, degrees

from lxml import etree
from shapely.geometry import LineString, Point

import inkex

from ..elements import Stroke
from ..i18n import _
from ..svg import get_correction_transform
from ..svg.tags import INKSCAPE_LABEL, INKSTITCH_ATTRIBS, SVG_PATH_TAG
from .base import InkstitchExtension


class CutworkSegmentation(InkstitchExtension):
    '''
    This will split up stroke elements according to their direction.
    Overlapping angle definitions (user input) will result in overlapping paths.
    This is wanted behaviour if the needles have a hard time to cut edges at the border of their specific angle capability.
    '''
    def __init__(self, *args, **kwargs):
        InkstitchExtension.__init__(self, *args, **kwargs)
        self.arg_parser.add_argument("-o", "--options", type=str, default=None, dest="page_1")
        self.arg_parser.add_argument("-i", "--info", type=str, default=None, dest="page_2")
        self.arg_parser.add_argument("-as", "--a_start", type=int, default=0, dest="a_start")
        self.arg_parser.add_argument("-ae", "--a_end", type=int, default=0, dest="a_end")
        self.arg_parser.add_argument("-ac", "--a_color", type=inkex.Color, default=inkex.Color(0x808080FF), dest="a_color")
        self.arg_parser.add_argument("-bs", "--b_start", type=int, default=0, dest="b_start")
        self.arg_parser.add_argument("-be", "--b_end", type=int, default=0, dest="b_end")
        self.arg_parser.add_argument("-bc", "--b_color", type=inkex.Color, default=inkex.Color(0x808080FF), dest="b_color")
        self.arg_parser.add_argument("-cs", "--c_start", type=int, default=0, dest="c_start")
        self.arg_parser.add_argument("-ce", "--c_end", type=int, default=0, dest="c_end")
        self.arg_parser.add_argument("-cc", "--c_color", type=inkex.Color, default=inkex.Color(0x808080FF), dest="c_color")
        self.arg_parser.add_argument("-ds", "--d_start", type=int, default=0, dest="d_start")
        self.arg_parser.add_argument("-de", "--d_end", type=int, default=0, dest="d_end")
        self.arg_parser.add_argument("-dc", "--d_color", type=inkex.Color, default=inkex.Color(0x808080FF), dest="d_color")
        self.arg_parser.add_argument("-s", "--sort_by_color", type=inkex.Boolean, default=True, dest="sort_by_color")
        self.arg_parser.add_argument("-k", "--keep_original", type=inkex.Boolean, default=False, dest="keep_original")

    def effect(self):
        if not self.svg.selection:
            inkex.errormsg(_("Please select one or more stroke elements."))
            return

        if not self.get_elements():
            return

        self.sectors = {
                        1: {'id': 1, 'start': self.options.a_start, 'end': self.options.a_end, 'color': self.options.a_color, 'point_list': []},
                        2: {'id': 2, 'start': self.options.b_start, 'end': self.options.b_end, 'color': self.options.b_color, 'point_list': []},
                        3: {'id': 3, 'start': self.options.c_start, 'end': self.options.c_end, 'color': self.options.c_color, 'point_list': []},
                        4: {'id': 4, 'start': self.options.d_start, 'end': self.options.d_end, 'color': self.options.d_color, 'point_list': []}
                       }

        # remove sectors where the start angle equals the end angle (some setups only work with two needles instead of four)
        self.sectors = {index: sector for index, sector in self.sectors.items() if sector['start'] != sector['end']}

        self.new_elements = []
        parent = None
        for element in self.elements:
            if isinstance(element, Stroke):
                # save parent and index to be able to position and insert new elements later on
                parent = element.node.getparent()
                index = parent.index(element.node)

                for path in element.paths:
                    linestring = LineString(path)
                    # fill self.new_elements list with line segments
                    self._prepare_line_sections(element, linestring.coords)

        if parent is None:
            inkex.errormsg(_("Please select at least one element with a stroke color."))
            return

        self._insert_elements(parent, index)
        self._remove_originals()

    def _get_sectors(self, angle):
        sectors = []
        for sector in self.sectors.values():
            if self._in_sector(angle, sector):
                sectors.append(sector)
        return sectors

    def _in_sector(self, angle, sector):
        stop = sector['end'] + 1
        if sector['start'] > stop:
            return angle in range(sector['start'], 181) or angle in range(0, stop)
        else:
            return angle in range(sector['start'], stop)

    def _get_angle(self, p1, p2):
        angle = round(degrees(atan2(p2.y - p1.y, p2.x - p1.x)) % 360)
        if angle > 180:
            angle -= 180
        return angle

    def _prepare_line_sections(self, element, coords):
        prev_point = None
        current_sectors = []

        for index, point in enumerate(coords):
            point = Point(*point)
            if prev_point is None:
                prev_point = point
                continue

            angle = self._get_angle(point, prev_point)
            sectors = self._get_sectors(angle)

            for sector in sectors:
                self.sectors[sector['id']]['point_list'].append(prev_point)
                # don't miss the last point
                if index == len(coords) - 1:
                    self.sectors[sector['id']]['point_list'].append(point)
                    self._prepare_element(self.sectors[sector['id']], element)

            # if a segment ends, prepare the element and clear point_lists
            for current in current_sectors:
                if current not in sectors:
                    # add last point
                    self.sectors[current['id']]['point_list'].append(prev_point)
                    self._prepare_element(self.sectors[current['id']], element)

            prev_point = point
            current_sectors = sectors

    def _prepare_element(self, sector, element):
        point_list = sector['point_list']
        if len(point_list) < 2:
            return

        color = str(self.path_style(element, str(sector['color'])))

        d = "M "
        for point in point_list:
            d += "%s,%s " % (point.x, point.y)

        stroke_element = etree.Element(SVG_PATH_TAG,
                                       {
                                        "style": color,
                                        "transform": get_correction_transform(element.node),
                                        INKSTITCH_ATTRIBS["ties"]: "3",
                                        INKSTITCH_ATTRIBS["running_stitch_length_mm"]: "1",
                                        INKSTITCH_ATTRIBS["cutwork_needle"]: str(sector['id']),
                                        "d": d
                                       })
        self.new_elements.append([stroke_element, sector['id']])
        # clear point_list in self.sectors
        self.sectors[sector['id']].update({'point_list': []})

    def _insert_elements(self, parent, index):
        self.new_elements.reverse()
        if self.options.sort_by_color is True:
            self.new_elements = sorted(self.new_elements, key=lambda x: x[1], reverse=True)

        group = self._insert_group(parent, _("Cutwork Group"), "__inkstitch_cutwork_group__", index)

        section = 0
        for element, section_id in self.new_elements:
            # if sorted by color, add a subgroup for each knife
            if self.options.sort_by_color:
                if section_id != section:
                    section = section_id
                    section_group = self._insert_group(group, _("Needle #%s") % section, "__inkstitch_cutwork_needle_group__")
            else:
                section_group = group

            section_group.insert(0, element)

    def _insert_group(self, parent, label, group_id, index=0):
        group = etree.Element("g", {
            INKSCAPE_LABEL: "%s" % label,
            "id": self.uniqueId("%s" % group_id)
        })
        parent.insert(index, group)
        return group

    def _remove_originals(self):
        if self.options.keep_original:
            return

        for element in self.elements:
            if isinstance(element, Stroke):
                parent = element.node.getparent()
                parent.remove(element.node)

    def path_style(self, element, color):
        # set stroke color and make it a running stitch - they don't want to cut zigzags
        return inkex.Style(element.node.get('style', '')) + inkex.Style('stroke:%s;stroke-dasharray:6,1;' % color)