diff options
Diffstat (limited to 'lib/sew_stack/stitch_layers/mixins')
| -rw-r--r-- | lib/sew_stack/stitch_layers/mixins/README.md | 9 | ||||
| -rw-r--r-- | lib/sew_stack/stitch_layers/mixins/path.py | 27 | ||||
| -rw-r--r-- | lib/sew_stack/stitch_layers/mixins/randomization.py | 121 |
3 files changed, 157 insertions, 0 deletions
diff --git a/lib/sew_stack/stitch_layers/mixins/README.md b/lib/sew_stack/stitch_layers/mixins/README.md new file mode 100644 index 00000000..12b6504b --- /dev/null +++ b/lib/sew_stack/stitch_layers/mixins/README.md @@ -0,0 +1,9 @@ +Functionality for StitchLayers is broken down into separate "mix-in" classes. +This allows us to divide up the implementation so that we don't end up with +one gigantic StitchLayer class. Individual StitchLayer subclasses can include +just the functionality they need. + +Python multiple inheritance is cool, and as long as we include a `super().__init__()` +call in every `__init__()` method we implement, we'll ensure that all mix-in +classes' constructors get called. Skipping implementing the `__init__()` method in a +mix-in class is also allowed. diff --git a/lib/sew_stack/stitch_layers/mixins/path.py b/lib/sew_stack/stitch_layers/mixins/path.py new file mode 100644 index 00000000..88e5419d --- /dev/null +++ b/lib/sew_stack/stitch_layers/mixins/path.py @@ -0,0 +1,27 @@ +from ..stitch_layer_editor import Category, Property +from ....i18n import _ +from ....utils import DotDict, Point + + +class PathPropertiesMixin: + @classmethod + def path_properties(cls): + return Category(_("Path")).children( + Property("reverse_path", _("Reverse path"), type=bool, + help=_("Reverse the path when stitching this layer.")) + ) + + +class PathMixin: + config: DotDict + paths: 'list[list[Point]]' + + def get_paths(self): + paths = self.paths + + if self.config.reverse_path: + paths.reverse() + for path in paths: + path.reverse() + + return paths diff --git a/lib/sew_stack/stitch_layers/mixins/randomization.py b/lib/sew_stack/stitch_layers/mixins/randomization.py new file mode 100644 index 00000000..5414731c --- /dev/null +++ b/lib/sew_stack/stitch_layers/mixins/randomization.py @@ -0,0 +1,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 |
