summaryrefslogtreecommitdiff
path: root/lib/tartan/palette.py
blob: 12d191a767b57efa986e7c21664dd9d3b02df995 (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
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
# Authors: see git history
#
# Copyright (c) 2023 Authors
# Licensed under the GNU GPL version 3.0 or later.  See the file LICENSE for details.
# Additional credits to: https://github.com/clsn/pyTartan

import re
from typing import List

import wx
from inkex import Color

from .colors import string_to_color


class Palette:
    """Holds information about the tartan palette"""
    def __init__(
        self,
        palette_code: str = '',
        palette_stripes: List[list] = [[], []],
        symmetry: bool = True,
        equal_warp_weft: bool = True,
        tt_unit: float = 0.5
    ) -> None:
        """
        :param palette_code: the palette code
        :param palette_stripes: the palette stripes, lists of warp and weft stripe dictionaries
        :param symmetry: reflective sett (True) / repeating sett (False)
        :param equal_warp_weft:wether warp and weft are equal or not
        :param tt_unit: mm per thread (used for the scottish register threadcount)
        """
        self.palette_code = palette_code
        self.palette_stripes = palette_stripes
        self.symmetry = symmetry
        self.equal_warp_weft = equal_warp_weft
        self.tt_unit = tt_unit

    def __repr__(self) -> str:
        return self.palette_code

    def update_symmetry(self, symmetry: bool) -> None:
        self.symmetry = symmetry
        self.update_code()

    def update_from_stripe_sizer(self, sizers: List[wx.BoxSizer], symmetry: bool = True, equal_warp_weft: bool = True) -> None:
        """
        Update palette code from stripes (customize panel)

        :param sizers: a list of the stripe sizers
        :param symmetry: reflective sett (True) / repeating sett (False)
        :param equal_warp_weft: wether warp and weft are equal or not
        """
        self.symmetry = symmetry
        self.equal_warp_weft = equal_warp_weft

        self.palette_stripes = [[], []]
        for i, outer_sizer in enumerate(sizers):
            stripes = []
            for stripe_sizer in outer_sizer.Children:
                stripe = {'render': True, 'color': '#000000', 'width': '5'}
                stripe_info = stripe_sizer.GetSizer()
                for color in stripe_info.GetChildren():
                    widget = color.GetWindow()
                    if isinstance(widget, wx.CheckBox):
                        # in embroidery it is ok to have gaps between the stripes
                        if not widget.GetValue():
                            stripe['render'] = False
                    elif isinstance(widget, wx.ColourPickerCtrl):
                        stripe['color'] = widget.GetColour().GetAsString(wx.C2S_HTML_SYNTAX)
                    elif isinstance(widget, wx.SpinCtrlDouble):
                        stripe['width'] = widget.GetValue()
                    elif isinstance(widget, wx.Button) or isinstance(widget, wx.StaticText):
                        continue
                stripes.append(stripe)
            self.palette_stripes[i] = stripes
            if self.equal_warp_weft:
                self.palette_stripes[1] = stripes
                break
        self.update_code()

    def update_from_code(self, code: str) -> None:
        """
        Update stripes (customize panel) according to the code applied by the user
        Converts code to valid Ink/Stitch code

        :param code: the tartan pattern code to apply
        """
        self.symmetry = True
        if '...' in code:
            self.symmetry = False
        self.equal_warp_weft = True
        if '|' in code:
            self.equal_warp_weft = False
        code = code.replace('/', '')
        code = code.replace('...', '')
        self.palette_stripes = [[], []]

        if "Threadcount" in code:
            self.parse_threadcount_code(code)
        elif '(' in code:
            self.parse_inkstitch_code(code)
        else:
            self.parse_simple_code(code)

        if self.equal_warp_weft:
            self.palette_stripes[1] = self.palette_stripes[0]

        self.update_code()

    def update_code(self) -> None:
        """Updates the palette code, reading from stripe settings (customize panel)"""
        code = []
        for i, direction in enumerate(self.palette_stripes):
            for stripe in direction:
                render = '' if stripe['render'] else '?'
                code.append(f"({stripe['color']}){render}{stripe['width']}")
            if i == 0 and self.equal_warp_weft is False:
                code.append("|")
            else:
                break
        if self.symmetry and len(code) > 0:
            code[0] = code[0].replace(')', ')/')
            code[-1] = code[-1].replace(')', ')/')
        code_str = ' '.join(code)
        if not self.symmetry:
            code_str = f'...{code}...'
        self.palette_code = code_str

    def parse_simple_code(self, code: str) -> None:
        """Example code:
        B24 W4 B24 R2 K24 G24 W2

        Each letter stands for a color defined in .colors.py (if not recognized, defaults to black)
        The number indicates the threadcount (width) of the stripe
        The width of one thread is user defined

        :param code: the tartan pattern code to apply
        """
        stripes = []
        stripe_info = re.findall(r'([a-zA-Z]+)(\?)?([0-9.]*)', code)
        for color, render, width in stripe_info:
            if not width:
                continue
            color = string_to_color(color)
            width = float(width) * self.tt_unit
            if not color:
                color = '#000000'
                render = '?'
            stripes.append({'render': not bool(render), 'color': color, 'width': float(width)})
        self.palette_stripes[0] = stripes

    def parse_inkstitch_code(self, code_str: str) -> None:
        """Example code:
        (#0000FF)/2.4 (#FFFFFF)0.4 (#0000FF)2.4 (#FF0000)0.2 (#000000)2.4 (#006400)2.4 (#FFFFFF)/0.2

        |   = separator warp and weft (if not equal)
        /   = indicates a symmetric sett
        ... = indicates an asymmetric sett

        :param code_str: the tartan pattern code to apply
        """
        code = code_str.split('|')
        for i, direction in enumerate(code):
            stripes = []
            stripe_info = re.findall(r'\(([0-9A-Za-z#]+)\)(\?)?([0-9.]+)', direction)
            for color, render, width in stripe_info:
                try:
                    # on macOS we need to run wxpython color method inside the app otherwise
                    # the color picker has issues in some cases to accept our input
                    color = wx.Colour(color).GetAsString(wx.C2S_HTML_SYNTAX)
                except wx.PyNoAppError:
                    # however when we render an embroidery element we do not want to open wx.App
                    color = str(Color(color).to_named())
                if not color:
                    color = '#000000'
                    render = False
                stripes.append({'render': not bool(render), 'color': color, 'width': float(width)})
            self.palette_stripes[i] = stripes

    def parse_threadcount_code(self, code: str) -> None:
        """Read in and work directly from a tartanregister.gov.uk threadcount response
        Example code:
            Threadcount:
            B24W4B24R2K24G24W2

            Palette:
            B=0000FFBLUE;W=FFFFFFWHITE;R=FF0000RED;K=000000BLACK;G=289C18GREEN;

            Threadcount given over a half sett with full count at the pivots.

        Colors in the threadcount are defined by Letters. The Palette section declares the rgb value

        :param code: the tartan pattern code to apply
        """
        if 'full sett' in code:
            self.symmetry = False
        else:
            self.symmetry = True

        colors = []
        thread_code = ''
        stripes = []
        lines = code.splitlines()
        i = 0
        while i < len(lines):
            line = lines[i].strip()
            if 'Threadcount:' in line and len(lines) > i:
                thread_code = lines[i+1]
            elif line.startswith('Palette:'):
                palette = lines[i+1]
                colors = re.findall(r'([A-Za-z]+)=#?([0-9afA-F]{6})', palette)
                color_dict = dict(colors)
            i += 1

        stripe_info = re.findall(r'([a-zA-Z]+)([0-9.]*)', thread_code)
        for color, width in stripe_info:
            render = True
            try:
                color = f'#{color_dict[color]}'
            except KeyError:
                color = '#000000'
                render = False
            width = float(width) * self.tt_unit
            stripes.append({'render': render, 'color': color, 'width': width})

        self.palette_stripes[0] = stripes

    def get_palette_width(self, scale: int, min_width: float, direction: int = 0) -> float:
        """
        Get the rendered width of the tartan palette
        :param scale: the scale value (percent) for the pattern
        :param min_width: min stripe width (before it is rendered as running stitch).
            Smaller stripes have 0 width.
        :param direction: 0 (warp) or 1 (weft)
        :returns: the width of all tartan stripes in given direction
        """
        width = 0
        for stripe in self.palette_stripes[direction]:
            stripe_width = stripe['width'] * (scale / 100)
            if stripe_width >= min_width or not stripe['render']:
                width += stripe_width
        return width