summaryrefslogtreecommitdiff
path: root/lib/sew_stack/stitch_layers/mixins/randomization.py
blob: 5414731c804ddc36369d748a9400af1f0be83ddf (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
import os
from secrets import randbelow
from typing import TYPE_CHECKING

import wx.propgrid

from ..stitch_layer_editor import Category, Property
from ....i18n import _
from ....svg import PIXELS_PER_MM
from ....utils import DotDict, get_resource_dir, prng

if TYPE_CHECKING:
    from ... import SewStack


editor_instance = None


class RandomSeedEditor(wx.propgrid.PGTextCtrlAndButtonEditor):
    def CreateControls(self, property_grid, property, position, size):
        if wx.SystemSettings().GetAppearance().IsDark():
            randomize_icon_file = 'randomize_20x20_dark.png'
        else:
            randomize_icon_file = 'randomize_20x20.png'
        randomize_icon = wx.Image(os.path.join(get_resource_dir("icons"), randomize_icon_file)).ConvertToBitmap()
        # button = wx.Button(property_grid, wx.ID_ANY, _("Re-roll"))
        # button.SetBitmap(randomize_icon)

        window_list = super().CreateControls(property_grid, property, position, size)
        button = window_list.GetSecondary()
        button.SetBitmap(randomize_icon)
        button.SetLabel("")
        button.SetToolTip(_("Re-roll"))
        return window_list


class RandomSeedProperty(wx.propgrid.IntProperty):
    def DoGetEditorClass(self):
        return wx.propgrid.PropertyGridInterface.GetEditorByName("RandomSeedEditor")

    def OnEvent(self, propgrid, primaryEditor, event):
        if event.GetEventType() == wx.wxEVT_COMMAND_BUTTON_CLICKED:
            self.SetValueInEvent(randbelow(int(1e8)))
            return True
        return False


class RandomizationPropertiesMixin:
    @classmethod
    def randomization_properties(cls):
        # We have to register the editor class once. We have to save a reference
        # to the editor to avoid letting it get garbage collected.
        global editor_instance
        if editor_instance is None:
            editor_instance = RandomSeedEditor()
            wx.propgrid.PropertyGrid.DoRegisterEditorClass(editor_instance, "RandomSeedEditor")

        return Category(_("Randomization")).children(
            Property("random_seed", _("Random seed"), type=RandomSeedProperty,
                     # Wow, it's really hard to explain the concept of a random seed to non-programmers...
                     help=_("The random seed is used when handling randomization settings.  " +
                            "Click the button to choose a new random seed, which will generate random features differently. " +
                            "Alternatively, you can enter your own random seed.  If you reuse a random seed, random features " +
                            "will look the same.")),
            Property("random_stitch_offset", _("Offset stitches"), unit="mm", prefix="±",
                     help=_("Move stitches randomly by up to this many millimeters in any direction.")),
            Property("random_stitch_path_offset", _("Offset stitch path"), unit="mm", prefix="±",
                     help=_("Move stitches randomly by up to this many millimeters perpendicular to the stitch path.\n\n" +
                            "If <b>Offset stitches</b> is also specified, then this one is processed first.")),
        )


class RandomizationMixin:
    config: DotDict
    element: "SewStack"

    @classmethod
    def randomization_defaults(cls):
        return dict(
            random_seed=None,
            random_stitch_offset=0.0,
            random_stitch_path_offset=0.0,
        )

    def get_random_seed(self):
        seed = self.config.get('random_seed')
        if seed is None:
            return self.element.get_default_random_seed()
        else:
            return seed

    def offset_stitches(self, stitches):
        """Randomly move stitches by modifying a list of stitches in-place."""

        if not stitches:
            return

        if 'random_stitch_path_offset' in self.config and self.config.random_stitch_path_offset:
            offset = self.config.random_stitch_path_offset * PIXELS_PER_MM
            rand_iter = iter(prng.iter_uniform_floats(self.get_random_seed(), "random_stitch_path_offset"))

            last_stitch = stitches[0]
            for stitch in stitches[1:]:
                try:
                    direction = (stitch - last_stitch).unit().rotate_left()
                except ZeroDivisionError:
                    continue

                last_stitch = stitch
                distance = next(rand_iter) * 2 * offset - offset
                result = stitch + direction * distance
                stitch.x = result.x
                stitch.y = result.y

        if 'random_stitch_offset' in self.config and self.config.random_stitch_offset:
            offset = self.config.random_stitch_offset * PIXELS_PER_MM
            rand_iter = iter(prng.iter_uniform_floats(self.get_random_seed(), "random_stitch_offset"))

            for stitch in stitches:
                stitch.x += next(rand_iter) * 2 * offset - offset
                stitch.y += next(rand_iter) * 2 * offset - offset