summaryrefslogtreecommitdiff
path: root/lib/sew_stack/stitch_layers/mixins
diff options
context:
space:
mode:
authorLex Neva <lexelby@users.noreply.github.com>2025-01-29 12:04:07 -0500
committerGitHub <noreply@github.com>2025-01-29 12:04:07 -0500
commit913c2700d1486284dba0583ae1b280b1aa237570 (patch)
treec165b29d0794981b5e44ab46f9838baab16b06a4 /lib/sew_stack/stitch_layers/mixins
parentefe3b27f17686094f74462bd81763a8197b54c6e (diff)
Sew Stack first steps (#3133)
* handle more recursive cases * scaffolding for stitch layers * scaffolding for SewStack * always use DotDict when parsing json params * add DefaultDotDict + DotDict fixes * first working SewStack (no UI yet) * ignore inkstitch_debug.log and .svg * refactor * early WIP: property grid display temporarily in stitch plan preview * start of sew stack editor extension * add layer properties panel and splitter * spacing and better icon * handle checkbox * add layer action buttons * show selected property help text in an HtmlWindow * rename * rephrase help text for tolerance * refactor into separate file * simplify structure * better property type handling * add randomization button * add random seed re-roll button * simulator preview * update preview in a few more cases * always DotDict * avoid ridiculously slow simulations * preview selected layer or all layers * edit multiple objects and save only modified properties into the SVG * better preview handling * add reverse and jitter * add stitch path jitter * fix types * fix random shuffle button * fixes * fix repeats * type hinting to please pycharm * show layer description * avoid exception in properties with multiple values * fix typing * fix new layer * draw a box around property grid and help box * confirm before closing * rename properties and fix seed * fix close/cancel logic * add buttons to undo changes and reset to default value * set not modified if default is original setting * fix invisible icon * more space for properties * fix random properties * better regulation of simulator rendering speed * Fixed timer being passed a float * fix get_json_param() default handling * fix tests * add checkbox for sew stack only * fix property help * adjustable stitch layer editor help box size, with persistence * repeat exact stitches * "fix" style * adjust for new next_element stuff --------- Co-authored-by: CapellanCitizen <thecapellancitizen@gmail.com>
Diffstat (limited to 'lib/sew_stack/stitch_layers/mixins')
-rw-r--r--lib/sew_stack/stitch_layers/mixins/README.md9
-rw-r--r--lib/sew_stack/stitch_layers/mixins/path.py27
-rw-r--r--lib/sew_stack/stitch_layers/mixins/randomization.py121
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