summaryrefslogtreecommitdiff
path: root/lib/gui
diff options
context:
space:
mode:
Diffstat (limited to 'lib/gui')
-rw-r--r--lib/gui/tartan/__init__.py10
-rw-r--r--lib/gui/tartan/code_panel.py59
-rw-r--r--lib/gui/tartan/customize_panel.py300
-rw-r--r--lib/gui/tartan/embroidery_panel.py201
-rw-r--r--lib/gui/tartan/help_panel.py42
-rw-r--r--lib/gui/tartan/main_panel.py271
6 files changed, 883 insertions, 0 deletions
diff --git a/lib/gui/tartan/__init__.py b/lib/gui/tartan/__init__.py
new file mode 100644
index 00000000..176d5d1e
--- /dev/null
+++ b/lib/gui/tartan/__init__.py
@@ -0,0 +1,10 @@
+# Authors: see git history
+#
+# Copyright (c) 2023 Authors
+# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
+
+from .code_panel import CodePanel
+from .customize_panel import CustomizePanel
+from .embroidery_panel import EmbroideryPanel
+from .help_panel import HelpPanel
+from .main_panel import TartanMainPanel
diff --git a/lib/gui/tartan/code_panel.py b/lib/gui/tartan/code_panel.py
new file mode 100644
index 00000000..f9dfe475
--- /dev/null
+++ b/lib/gui/tartan/code_panel.py
@@ -0,0 +1,59 @@
+# Authors: see git history
+#
+# Copyright (c) 2023 Authors
+# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
+
+import wx
+import wx.adv
+
+from ...i18n import _
+
+
+class CodePanel(wx.Panel):
+ def __init__(self, parent, panel):
+ self.panel = panel
+ wx.Panel.__init__(self, parent)
+ code_sizer = wx.BoxSizer(wx.VERTICAL)
+ load_palette_sizer = wx.BoxSizer(wx.HORIZONTAL)
+ tt_unit_sizer = wx.BoxSizer(wx.HORIZONTAL)
+
+ self.threadcount_text = wx.TextCtrl(self, style=wx.TE_MULTILINE)
+ self.threadcount_text.Bind(wx.EVT_TEXT, self.set_tt_unit_status)
+ code_sizer.Add(self.threadcount_text, 1, wx.EXPAND | wx.ALL, 10)
+
+ self.tt_unit_label = wx.StaticText(self, label=_("1 Tartan thread equals (mm)"))
+ self.tt_unit_spin = wx.SpinCtrlDouble(self, min=0.01, max=50, initial=0.2, inc=0.1, style=wx.SP_WRAP)
+ self.tt_unit_spin.SetDigits(2)
+ tt_unit_sizer.Add(self.tt_unit_label, 0, wx.CENTER | wx.ALL, 10)
+ tt_unit_sizer.Add(self.tt_unit_spin, 0, wx.ALL, 10)
+ self.tt_unit_label.SetToolTip(_("Used only for Threadcount code (The Scottish Register of Tartans)"))
+ self.tt_unit_spin.SetToolTip(_("Used only for Threadcount code (The Scottish Register of Tartans)"))
+
+ code_sizer.Add(tt_unit_sizer, 0, wx.ALL, 10)
+
+ load_button = wx.Button(self, label="Apply Code")
+ load_button.Bind(wx.EVT_BUTTON, self._load_palette_code)
+ load_palette_sizer.Add(load_button, 0, wx.ALL, 10)
+
+ code_sizer.Add(load_palette_sizer, 0, wx.ALL, 10)
+
+ self.SetSizer(code_sizer)
+
+ def _load_palette_code(self, event):
+ self.panel.palette.tt_unit = self.tt_unit_spin.GetValue()
+ self.panel.update_from_code()
+ self.panel.settings['palette'] = self.threadcount_text.GetValue()
+
+ def set_tt_unit_status(self, event):
+ # we always want to convert the width into mm
+ # when threadcount code is given we have to enable the threadcount unit field
+ # so they can define the mm-width of one tartan thread
+ threadcount_text = self.threadcount_text.GetValue()
+ if '(' in threadcount_text and 'Threadcount' not in threadcount_text:
+ # depending on how much of the mailed text is copied into the code field,
+ # we may have brackets in there (1997). So let's also check for "threadcount"
+ self.tt_unit_label.Enable(False)
+ self.tt_unit_spin.Enable(False)
+ else:
+ self.tt_unit_label.Enable(True)
+ self.tt_unit_spin.Enable(True)
diff --git a/lib/gui/tartan/customize_panel.py b/lib/gui/tartan/customize_panel.py
new file mode 100644
index 00000000..16d73416
--- /dev/null
+++ b/lib/gui/tartan/customize_panel.py
@@ -0,0 +1,300 @@
+# Authors: see git history
+#
+# Copyright (c) 2023 Authors
+# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
+
+from math import floor
+
+import wx
+from wx.lib.scrolledpanel import ScrolledPanel
+
+from ...i18n import _
+
+
+class CustomizePanel(ScrolledPanel):
+
+ def __init__(self, parent, panel):
+ self.panel = panel
+ self.mouse_position = None
+ ScrolledPanel.__init__(self, parent)
+
+ self.customize_sizer = wx.BoxSizer(wx.VERTICAL)
+ general_settings_sizer = wx.BoxSizer(wx.HORIZONTAL)
+ positional_settings_sizer = wx.FlexGridSizer(2, 4, 5, 5)
+ stripe_header_sizer = wx.BoxSizer(wx.HORIZONTAL)
+ self.stripe_sizer = wx.BoxSizer(wx.HORIZONTAL)
+ self.warp_outer_sizer = wx.BoxSizer(wx.VERTICAL)
+ self.weft_outer_sizer = wx.BoxSizer(wx.VERTICAL)
+ self.warp_sizer = wx.BoxSizer(wx.VERTICAL)
+ self.weft_sizer = wx.BoxSizer(wx.VERTICAL)
+
+ general_settings_headline = wx.StaticText(self, label=_("Pattern Settings"))
+ general_settings_headline.SetFont(wx.Font().Bold())
+ self.symmetry_checkbox = wx.CheckBox(self, label=_("Symmetrical / reflective sett"))
+ self.symmetry_checkbox.SetToolTip(_("Disabled: asymmetrical / repeating sett"))
+ self.symmetry_checkbox.Bind(wx.EVT_CHECKBOX, self.update_symmetry)
+ self.warp_weft_checkbox = wx.CheckBox(self, label=_("Equal threadcount for warp and weft"))
+ self.warp_weft_checkbox.Bind(wx.EVT_CHECKBOX, self._update_warp_weft_event)
+
+ positional_settings_headline = wx.StaticText(self, label=_("Position"))
+ positional_settings_headline.SetFont(wx.Font().Bold())
+ self.rotate = wx.SpinCtrlDouble(self, min=-180, max=180, initial=0, inc=0.1, style=wx.SP_WRAP)
+ self.rotate.SetDigits(2)
+ self.rotate.Bind(wx.EVT_SPINCTRLDOUBLE, lambda event: self.on_change("rotate", event))
+ rotate_label = wx.StaticText(self, label=_("Rotate"))
+ self.scale = wx.SpinCtrl(self, min=0, max=1000, initial=100, style=wx.SP_WRAP)
+ self.scale.Bind(wx.EVT_SPINCTRL, self.update_scale)
+ scale_label = wx.StaticText(self, label=_("Scale (%)"))
+ self.offset_x = wx.SpinCtrlDouble(self, min=0, max=500, initial=0, style=wx.SP_WRAP)
+ self.offset_x.Bind(wx.EVT_SPINCTRLDOUBLE, lambda event: self.on_change("offset_x", event))
+ self.offset_x.SetDigits(2)
+ offset_x_label = wx.StaticText(self, label=_("Offset X (mm)"))
+ self.offset_y = wx.SpinCtrlDouble(self, min=0, max=500, initial=0, style=wx.SP_WRAP)
+ self.offset_y.Bind(wx.EVT_SPINCTRLDOUBLE, lambda event: self.on_change("offset_y", event))
+ self.offset_y.SetDigits(2)
+ offset_y_label = wx.StaticText(self, label=_("Offset Y (mm)"))
+
+ stripe_settings_headline = wx.StaticText(self, label=_("Stripes"))
+ stripe_settings_headline.SetFont(wx.Font().Bold())
+ self.link_colors_checkbox = wx.CheckBox(self, label=_("Link colors"))
+ self.link_colors_checkbox.SetToolTip(_("When enabled update all equal colors simultaneously."))
+ self.warp_headline = wx.StaticText(self, label=_("Warp"))
+ self.warp_headline.SetFont(wx.Font().Bold())
+ self.weft_headline = wx.StaticText(self, label=_("Weft"))
+ self.weft_headline.SetFont(wx.Font().Bold())
+ self.add_warp_button = wx.Button(self, label=_("Add"))
+ self.add_warp_button.Bind(wx.EVT_BUTTON, self._add_warp_event)
+ self.add_weft_button = wx.Button(self, label=_("Add"))
+ self.add_weft_button.Bind(wx.EVT_BUTTON, self._add_weft_event)
+
+ # Add to sizers
+
+ general_settings_sizer.Add(self.symmetry_checkbox, 0, wx.CENTER | wx.ALL, 10)
+ general_settings_sizer.Add(self.warp_weft_checkbox, 0, wx.CENTER | wx.ALL, 10)
+
+ positional_settings_sizer.Add(rotate_label, 0, wx.ALIGN_CENTRE, 0)
+ positional_settings_sizer.Add(self.rotate, 0, wx.EXPAND | wx.RIGHT, 30)
+ positional_settings_sizer.Add(offset_x_label, 0, wx.ALIGN_CENTRE, 0)
+ positional_settings_sizer.Add(self.offset_x, 0, wx.EXPAND, 0)
+ positional_settings_sizer.Add(scale_label, 0, wx.ALIGN_CENTRE, 0)
+ positional_settings_sizer.Add(self.scale, 0, wx.EXPAND | wx.RIGHT, 30)
+ positional_settings_sizer.Add(offset_y_label, 0, wx.ALIGN_CENTRE, 0)
+ positional_settings_sizer.Add(self.offset_y, 0, wx.EXPAND, 0)
+ positional_settings_sizer.AddGrowableCol(1)
+ positional_settings_sizer.AddGrowableCol(3)
+
+ self.warp_outer_sizer.Add(self.warp_headline, 0, wx.EXPAND, 0)
+ self.weft_outer_sizer.Add(self.weft_headline, 0, wx.EXPAND, 0)
+ self.warp_outer_sizer.Add(self.warp_sizer, 0, wx.EXPAND, 0)
+ self.weft_outer_sizer.Add(self.weft_sizer, 0, wx.EXPAND, 0)
+ self.warp_outer_sizer.Add(self.add_warp_button, 0, wx.ALIGN_RIGHT | wx.ALL, 10)
+ self.weft_outer_sizer.Add(self.add_weft_button, 0, wx.ALIGN_RIGHT | wx.ALL, 10)
+ self.stripe_sizer.Add(self.warp_outer_sizer, 1, wx.EXPAND, 0)
+ self.stripe_sizer.Add(self.weft_outer_sizer, 1, wx.EXPAND, 0)
+
+ stripe_header_sizer.Add(stripe_settings_headline, 0, wx.ALL, 10)
+ stripe_header_sizer.Add((0, 0), 1, wx.ALL | wx.EXPAND, 10)
+ stripe_header_sizer.Add(self.link_colors_checkbox, 0, wx.ALL, 10)
+
+ self.customize_sizer.Add(positional_settings_headline, 0, wx.ALL, 10)
+ self.customize_sizer.Add(positional_settings_sizer, 0, wx.ALL | wx.EXPAND, 10)
+ self.customize_sizer.Add(wx.StaticLine(self), 0, wx.ALL | wx.EXPAND, 10)
+ self.customize_sizer.Add(general_settings_headline, 0, wx.ALL, 10)
+ self.customize_sizer.Add(general_settings_sizer, 0, wx.ALL | wx.EXPAND, 10)
+ self.customize_sizer.Add(wx.StaticLine(self), 0, wx.ALL | wx.EXPAND, 10)
+ self.customize_sizer.Add(stripe_header_sizer, 0, wx.EXPAND | wx.ALL, 10)
+ self.customize_sizer.Add(self.stripe_sizer, 0, wx.EXPAND | wx.ALL, 10)
+
+ self.SetSizer(self.customize_sizer)
+
+ def _add_warp_event(self, event):
+ self.add_stripe()
+
+ def _add_weft_event(self, event):
+ self.add_stripe(False)
+
+ def add_stripe(self, warp=True, stripe=None, update=True):
+ stripesizer = wx.BoxSizer(wx.HORIZONTAL)
+
+ position = wx.Button(self, label='⁝', style=wx.BU_EXACTFIT)
+ position.SetToolTip(_("Drag and drop to adjust position."))
+ position.Bind(wx.EVT_LEFT_DOWN, self._move_stripe_start)
+ position.Bind(wx.EVT_LEFT_UP, self._move_stripe_end)
+
+ visibility = wx.CheckBox(self)
+ visibility.SetToolTip(_("Stitch this stripe"))
+ visibility.SetValue(True)
+ visibility.Bind(wx.EVT_CHECKBOX, self._update_stripes_event)
+
+ # hidden label used for linked colors
+ # there seems to be no native way to catch the old color setting
+ colorinfo = wx.StaticText(self, label='black')
+ colorinfo.Hide()
+
+ colorpicker = wx.ColourPickerCtrl(self, colour=wx.Colour('black'))
+ colorpicker.SetToolTip(_("Select stripe color"))
+ colorpicker.Bind(wx.EVT_COLOURPICKER_CHANGED, self._update_color)
+
+ stripe_width = wx.SpinCtrlDouble(self, min=0.01, max=500, initial=5, style=wx.SP_WRAP)
+ stripe_width.SetDigits(2)
+ stripe_width.SetToolTip(_("Set stripe width (mm)"))
+ stripe_width.Bind(wx.EVT_SPINCTRLDOUBLE, self._update_stripes_event)
+
+ remove_button = wx.Button(self, label='X')
+ remove_button.SetToolTip(_("Remove stripe"))
+ remove_button.Bind(wx.EVT_BUTTON, self._remove_stripe)
+
+ stripesizer.Add(position, 0, wx.CENTER | wx.RIGHT | wx.TOP, 5)
+ stripesizer.Add(visibility, 0, wx.CENTER | wx.RIGHT | wx.TOP, 5)
+ stripesizer.Add(colorinfo, 0, wx.RIGHT | wx.TOP, 5)
+ stripesizer.Add(colorpicker, 0, wx.RIGHT | wx.TOP, 5)
+ stripesizer.Add(stripe_width, 1, wx.RIGHT | wx.TOP, 5)
+ stripesizer.Add(remove_button, 0, wx.CENTER | wx.TOP, 5)
+
+ if stripe is not None:
+ visibility.SetValue(stripe['render'])
+ colorinfo.SetLabel(wx.Colour(stripe['color']).GetAsString(wx.C2S_HTML_SYNTAX))
+ colorpicker.SetColour(wx.Colour(stripe['color']))
+ stripe_width.SetValue(stripe['width'])
+ if warp:
+ self.warp_sizer.Add(stripesizer, 0, wx.EXPAND | wx.ALL, 5)
+ else:
+ self.weft_sizer.Add(stripesizer, 0, wx.EXPAND | wx.ALL, 5)
+ if update:
+ self.panel.update_from_stripes()
+ self.set_stripe_width_color(stripe_width)
+ self.FitInside()
+
+ def _move_stripe_start(self, event):
+ self.mouse_position = wx.GetMousePosition()
+
+ def _move_stripe_end(self, event):
+ stripe = event.GetEventObject()
+ sizer = stripe.GetContainingSizer()
+ if self.warp_sizer.GetItem(sizer):
+ main_sizer = self.warp_sizer
+ else:
+ main_sizer = self.weft_sizer
+ for i, item in enumerate(main_sizer.GetChildren()):
+ if item.GetSizer() == sizer:
+ index = i
+ break
+ position = wx.GetMousePosition()
+ sizer_height = sizer.GetSize()[1] + 10
+ move = floor((position[1] - self.mouse_position[1]) / sizer_height)
+ index = min(len(main_sizer.Children) - 1, max(0, (index + move)))
+ main_sizer.Detach(sizer)
+ main_sizer.Insert(index, sizer, 0, wx.EXPAND | wx.ALL, 5)
+ self.panel.update_from_stripes()
+ self.FitInside()
+
+ def _remove_stripe(self, event):
+ sizer = event.GetEventObject().GetContainingSizer()
+ sizer.Clear(True)
+ self.warp_sizer.Remove(sizer)
+ try:
+ self.weft_sizer.Remove(sizer)
+ except RuntimeError:
+ # we may have removed it already
+ pass
+ self.panel.update_from_stripes()
+ self.FitInside()
+
+ def on_change(self, attribute, event):
+ self.panel.settings[attribute] = event.EventObject.GetValue()
+ self.panel.update_preview()
+
+ def update_scale(self, event):
+ self.panel.settings['scale'] = event.EventObject.GetValue()
+ # self.update_stripes(self.panel.palette.palette_stripes)
+ self.update_stripe_width_colors()
+ self.panel.update_preview()
+
+ def _update_stripes_event(self, event):
+ self.set_stripe_width_color(event.EventObject)
+ self.panel.update_from_stripes()
+
+ def update_stripe_width_colors(self):
+ for sizer in [self.warp_sizer, self.weft_sizer]:
+ for stripe_sizer in sizer.GetChildren():
+ inner_sizer = stripe_sizer.GetSizer()
+ for stripe_widget in inner_sizer:
+ widget = stripe_widget.GetWindow()
+ if isinstance(widget, wx.SpinCtrlDouble):
+ self.set_stripe_width_color(widget)
+
+ def set_stripe_width_color(self, stripe_width_ctrl):
+ scale = self.scale.GetValue()
+ min_stripe_width = self.panel.embroidery_panel.min_stripe_width.GetValue()
+ stripe_width = stripe_width_ctrl.GetValue() * scale / 100
+ if stripe_width <= min_stripe_width:
+ stripe_width_ctrl.SetBackgroundColour(wx.Colour('#efefef'))
+ stripe_width_ctrl.SetForegroundColour('black')
+ else:
+ stripe_width_ctrl.SetBackgroundColour(wx.NullColour)
+ stripe_width_ctrl.SetForegroundColour(wx.NullColour)
+
+ def update_stripes(self, stripes):
+ self.warp_sizer.Clear(True)
+ self.weft_sizer.Clear(True)
+ warp = True
+ for direction in stripes:
+ for stripe in direction:
+ self.add_stripe(warp, stripe, False)
+ warp = False
+ self.panel.update_from_stripes()
+
+ def _update_color(self, event):
+ linked = self.link_colors_checkbox.GetValue()
+ widget = event.GetEventObject()
+ colorinfo = widget.GetPrevSibling()
+ old_color = wx.Colour(colorinfo.GetLabel())
+ new_color = event.Colour
+ if linked:
+ self._update_color_picker(old_color, new_color, self.warp_sizer)
+ self._update_color_picker(old_color, new_color, self.weft_sizer)
+ colorinfo.SetLabel(new_color.GetAsString(wx.C2S_HTML_SYNTAX))
+ self.panel.update_from_stripes()
+
+ def _update_color_picker(self, old_color, new_color, sizer):
+ for stripe_sizer in sizer.Children:
+ stripe_info = stripe_sizer.GetSizer()
+ for widget in stripe_info.GetChildren():
+ widget = widget.GetWindow()
+ if isinstance(widget, wx.ColourPickerCtrl):
+ color = widget.GetColour()
+ if color == old_color:
+ widget.SetColour(new_color)
+ widget.GetPrevSibling().SetLabel(new_color.GetAsString(wx.C2S_HTML_SYNTAX))
+
+ def update_symmetry(self, event=None):
+ symmetry = self.symmetry_checkbox.GetValue()
+ self.panel.settings['symmetry'] = symmetry
+ self.panel.palette.update_symmetry(symmetry)
+ self.panel.update_from_stripes()
+ self.FitInside()
+
+ def update_warp_weft(self):
+ equal_warp_weft = self.warp_weft_checkbox.GetValue()
+ if equal_warp_weft:
+ self.stripe_sizer.Hide(self.warp_headline, recursive=True)
+ self.stripe_sizer.Hide(self.weft_outer_sizer, recursive=True)
+ else:
+ self.stripe_sizer.Show(self.warp_headline, recursive=True)
+ self.stripe_sizer.Show(self.weft_outer_sizer, recursive=True)
+ # We just made the weft colorinfo visible. Let's hide it again.
+ self._hide_colorinfo()
+ self.FitInside()
+
+ def _update_warp_weft_event(self, event):
+ self.panel.settings['equal_warp_weft'] = event.GetEventObject().GetValue()
+ self.update_warp_weft()
+ self.panel.update_from_stripes()
+
+ def _hide_colorinfo(self):
+ for stripe_sizer in self.weft_sizer.Children:
+ stripe_info = stripe_sizer.GetSizer()
+ for stripe in stripe_info.GetChildren():
+ widget = stripe.GetWindow()
+ if isinstance(widget, wx.StaticText):
+ widget.Hide()
diff --git a/lib/gui/tartan/embroidery_panel.py b/lib/gui/tartan/embroidery_panel.py
new file mode 100644
index 00000000..f3b756d7
--- /dev/null
+++ b/lib/gui/tartan/embroidery_panel.py
@@ -0,0 +1,201 @@
+# Authors: see git history
+#
+# Copyright (c) 2023 Authors
+# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
+
+import wx
+
+from ...i18n import _
+from ...utils.param import ParamOption
+
+
+class EmbroideryPanel(wx.Panel):
+ def __init__(self, parent, panel):
+ self.panel = panel
+ wx.Panel.__init__(self, parent)
+
+ self.embroidery_sizer = wx.BoxSizer(wx.VERTICAL)
+ self.embroidery_element_sizer = wx.FlexGridSizer(6, 2, 5, 5)
+ self.embroidery_element_sizer.AddGrowableCol(1)
+ self.svg_elements_sizer = wx.FlexGridSizer(6, 2, 5, 5)
+ self.svg_elements_sizer.AddGrowableCol(1)
+ self.common_settings_sizer = wx.FlexGridSizer(1, 2, 5, 5)
+ self.common_settings_sizer.AddGrowableCol(1)
+
+ help_text = wx.StaticText(self, -1, _("Embroidery settings can be refined in the params dialog."))
+
+ # Method
+ self.output_method = wx.ComboBox(self, choices=[], style=wx.CB_READONLY)
+ for choice in embroider_choices:
+ self.output_method.Append(choice.name, choice)
+ self.output_method.SetSelection(0)
+ self.output_method.Bind(wx.EVT_COMBOBOX, self.update_output_method)
+
+ # Embroidery Element Params
+ stitch_angle_label = wx.StaticText(self, label=_("Angle of lines of stitches"))
+ stitch_angle_label.SetToolTip(_('Relative to the tartan stripe direction.'))
+ self.stitch_angle = wx.SpinCtrlDouble(self, min=-90, max=90, initial=-45, style=wx.SP_WRAP)
+ self.stitch_angle.SetDigits(2)
+ self.stitch_angle.SetIncrement(1)
+ self.stitch_angle.Bind(wx.EVT_SPINCTRLDOUBLE, lambda event: self.on_param_change("tartan_angle", event))
+
+ rows_per_thread_label = wx.StaticText(self, label=_("Rows per tartan thread"))
+ self.rows_per_thread = wx.SpinCtrl(self, min=1, max=50, initial=2, style=wx.SP_WRAP)
+ lines_text = _("Consecutive rows of the same color")
+ rows_per_thread_label.SetToolTip(lines_text)
+ self.rows_per_thread.SetToolTip(lines_text)
+ self.rows_per_thread.Bind(wx.EVT_SPINCTRL, lambda event: self.on_param_change("rows_per_thread", event))
+
+ row_spacing_label = wx.StaticText(self, label=_("Row spacing (mm)"))
+ self.row_spacing = wx.SpinCtrlDouble(self, min=0.01, max=500, initial=0.25, style=wx.SP_WRAP)
+ self.row_spacing.SetDigits(2)
+ self.row_spacing.SetIncrement(0.01)
+ self.row_spacing.Bind(wx.EVT_SPINCTRLDOUBLE, lambda event: self.on_param_change("row_spacing_mm", event))
+
+ underlay_label = wx.StaticText(self, label=_("Underlay"))
+ self.underlay = wx.CheckBox(self)
+ self.underlay.Bind(wx.EVT_CHECKBOX, lambda event: self.on_param_change("fill_underlay", event))
+
+ herringbone_label = wx.StaticText(self, label=_("Herringbone width (mm)"))
+ self.herringbone = wx.SpinCtrlDouble(self, min=0, max=500, initial=0, style=wx.SP_WRAP)
+ self.herringbone.SetDigits(2)
+ self.herringbone.SetIncrement(1)
+ self.herringbone.Bind(wx.EVT_SPINCTRLDOUBLE, lambda event: self.on_param_change("herringbone_width_mm", event))
+
+ bean_stitch_repeats_label = wx.StaticText(self, label=_("Bean stitch repeats"))
+ self.bean_stitch_repeats = wx.TextCtrl(self)
+ self.bean_stitch_repeats.Bind(wx.EVT_TEXT, lambda event: self.on_param_change("bean_stitch_repeats", event))
+
+ # SVG Output Settings
+ stitch_type_label = wx.StaticText(self, label=_("Stitch type"))
+ self.stitch_type = wx.ComboBox(self, choices=[], style=wx.CB_READONLY)
+ for choice in stitch_type_choices:
+ self.stitch_type.Append(choice.name, choice)
+ self.stitch_type.SetSelection(0)
+ self.stitch_type.Bind(wx.EVT_COMBOBOX, self.on_change_stitch_type)
+
+ svg_row_spacing_label = wx.StaticText(self, label=_("Row spacing"))
+ self.svg_row_spacing = wx.SpinCtrlDouble(self, min=0.01, max=500, initial=1, style=wx.SP_WRAP)
+ self.svg_row_spacing.SetDigits(2)
+ self.row_spacing.SetIncrement(0.01)
+ self.svg_row_spacing.Bind(wx.EVT_SPINCTRLDOUBLE, lambda event: self.on_change("row_spacing", event))
+
+ angle_warp_label = wx.StaticText(self, label=_("Stitch angle (warp)"))
+ self.angle_warp = wx.SpinCtrl(self, min=-90, max=90, initial=0, style=wx.SP_WRAP)
+ self.angle_warp.Bind(wx.EVT_SPINCTRL, lambda event: self.on_change("angle_warp", event))
+
+ angle_weft_label = wx.StaticText(self, label=_("Stitch angle (weft)"))
+ self.angle_weft = wx.SpinCtrl(self, min=-90, max=90, initial=90, style=wx.SP_WRAP)
+ self.angle_weft.Bind(wx.EVT_SPINCTRL, lambda event: self.on_change("angle_weft", event))
+
+ min_stripe_width_label = wx.StaticText(self, label=_("Minimum stripe width for fills"))
+ self.min_stripe_width = wx.SpinCtrlDouble(self, min=0, max=100, initial=1, style=wx.SP_WRAP)
+ self.min_stripe_width.SetDigits(2)
+ self.row_spacing.SetIncrement(0.1)
+ min_width_text = _("Stripes smaller than this will be stitched as a running stitch")
+ min_stripe_width_label.SetToolTip(min_width_text)
+ self.min_stripe_width.SetToolTip(min_width_text)
+ self.min_stripe_width.Bind(wx.EVT_SPINCTRLDOUBLE, self.on_change_min_stripe_width)
+
+ svg_bean_stitch_repeats_label = wx.StaticText(self, label=_("Bean stitch repeats"))
+ self.svg_bean_stitch_repeats = wx.SpinCtrl(self, min=0, max=10, initial=0, style=wx.SP_WRAP)
+ self.svg_bean_stitch_repeats.Bind(wx.EVT_SPINCTRL, lambda event: self.on_change("bean_stitch_repeats", event))
+
+ # Add to sizers
+ self.embroidery_element_sizer.Add(stitch_angle_label, 0, wx.ALIGN_CENTRE, 0)
+ self.embroidery_element_sizer.Add(self.stitch_angle, 0, wx.EXPAND, 0)
+ self.embroidery_element_sizer.Add(rows_per_thread_label, 0, wx.ALIGN_CENTRE, 0)
+ self.embroidery_element_sizer.Add(self.rows_per_thread, 0, wx.EXPAND, 0)
+ self.embroidery_element_sizer.Add(row_spacing_label, 0, wx.ALIGN_CENTRE, 0)
+ self.embroidery_element_sizer.Add(self.row_spacing, 0, wx.EXPAND, 0)
+ self.embroidery_element_sizer.Add(herringbone_label, 0, wx.ALIGN_CENTRE, 0)
+ self.embroidery_element_sizer.Add(self.herringbone, 0, wx.EXPAND, 0)
+ self.embroidery_element_sizer.Add(underlay_label, 0, wx.ALIGN_CENTRE, 0)
+ self.embroidery_element_sizer.Add(self.underlay, 0, wx.EXPAND, 0)
+ self.embroidery_element_sizer.Add(bean_stitch_repeats_label, 0, wx.ALIGN_CENTRE, 0)
+ self.embroidery_element_sizer.Add(self.bean_stitch_repeats, 0, wx.EXPAND, 0)
+
+ self.svg_elements_sizer.Add(stitch_type_label, 0, wx.ALIGN_CENTRE, 0)
+ self.svg_elements_sizer.Add(self.stitch_type, 0, wx.EXPAND, 0)
+ self.svg_elements_sizer.Add(svg_row_spacing_label, 0, wx.ALIGN_CENTRE, 0)
+ self.svg_elements_sizer.Add(self.svg_row_spacing, 0, wx.EXPAND, 0)
+ self.svg_elements_sizer.Add(angle_warp_label, 0, wx.ALIGN_CENTRE, 0)
+ self.svg_elements_sizer.Add(self.angle_warp, 0, wx.EXPAND, 0)
+ self.svg_elements_sizer.Add(angle_weft_label, 0, wx.ALIGN_CENTRE, 0)
+ self.svg_elements_sizer.Add(self.angle_weft, 0, wx.EXPAND, 0)
+ self.svg_elements_sizer.Add(svg_bean_stitch_repeats_label, 0, wx.ALIGN_CENTRE, 0)
+ self.svg_elements_sizer.Add(self.svg_bean_stitch_repeats, 0, wx.EXPAND, 0)
+
+ self.common_settings_sizer.Add(min_stripe_width_label, 0, wx.ALIGN_CENTRE, 0)
+ self.common_settings_sizer.Add(self.min_stripe_width, 0, wx.EXPAND, 0)
+
+ self.embroidery_sizer.Add(self.output_method, 0, wx.EXPAND | wx.ALL, 10)
+ self.embroidery_sizer.Add(self.embroidery_element_sizer, 0, wx.EXPAND | wx.ALL, 10)
+ self.embroidery_sizer.Add(self.svg_elements_sizer, 0, wx.EXPAND | wx.ALL, 10)
+ self.embroidery_sizer.Add(self.common_settings_sizer, 0, wx.EXPAND | wx.ALL, 10)
+ self.embroidery_sizer.Add(wx.StaticLine(self), 0, wx.ALL | wx.EXPAND, 10)
+ self.embroidery_sizer.Add(help_text, 0, wx.EXPAND | wx.ALL, 10)
+ self.embroidery_sizer.Add(wx.StaticLine(self), 0, wx.ALL | wx.EXPAND, 10)
+
+ self.embroidery_sizer.Hide(self.svg_elements_sizer)
+ self.SetSizer(self.embroidery_sizer)
+
+ def update_output_method(self, event):
+ output = self.output_method.GetClientData(self.output_method.GetSelection()).id
+ if output == "svg":
+ self.embroidery_sizer.Show(self.svg_elements_sizer)
+ self.embroidery_sizer.Hide(self.embroidery_element_sizer)
+ for element in self.panel.elements:
+ element.pop('inkstitch:fill_method')
+ else:
+ self.embroidery_sizer.Show(self.embroidery_element_sizer)
+ self.embroidery_sizer.Hide(self.svg_elements_sizer)
+ for element in self.panel.elements:
+ element.set('inkstitch:fill_method', 'tartan_fill')
+ self.panel.settings['output'] = output
+ self.Layout()
+ self.panel.update_preview()
+
+ def set_output(self, choice):
+ for option in embroider_choices:
+ if option.id == choice:
+ self.output_method.SetValue(option.name)
+ self.update_output_method(None)
+ break
+
+ def on_change(self, attribute, event):
+ self.panel.settings[attribute] = event.GetEventObject().GetValue()
+ self.panel.update_preview()
+
+ def on_change_stitch_type(self, event):
+ stitch_type = self.stitch_type.GetClientData(self.stitch_type.GetSelection()).id
+ self.panel.settings['stitch_type'] = stitch_type
+ self.panel.update_preview()
+
+ def on_change_min_stripe_width(self, event):
+ self.panel.settings['min_stripe_width'] = event.EventObject.GetValue()
+ self.panel.customize_panel.update_stripe_width_colors()
+ self.panel.update_preview()
+
+ def on_param_change(self, attribute, event):
+ for element in self.panel.elements:
+ element.set(f'inkstitch:{attribute}', str(event.GetEventObject().GetValue()))
+ self.panel.update_preview()
+
+ def set_stitch_type(self, choice):
+ for option in stitch_type_choices:
+ if option.id == choice:
+ self.stitch_type.SetValue(option.name)
+ break
+
+
+embroider_choices = [
+ ParamOption("embroidery", _("Embroidery Element")),
+ ParamOption("svg", _("SVG Elements"))
+]
+
+
+stitch_type_choices = [
+ ParamOption("auto_fill", _("AutoFill")),
+ ParamOption("legacy_fill", _("Legacy Fill"))
+]
diff --git a/lib/gui/tartan/help_panel.py b/lib/gui/tartan/help_panel.py
new file mode 100644
index 00000000..1e2142d9
--- /dev/null
+++ b/lib/gui/tartan/help_panel.py
@@ -0,0 +1,42 @@
+# Authors: see git history
+#
+# Copyright (c) 2023 Authors
+# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
+
+import wx
+
+from ...i18n import _
+
+
+class HelpPanel(wx.Panel):
+ def __init__(self, parent):
+ wx.Panel.__init__(self, parent)
+ help_sizer = wx.BoxSizer(wx.VERTICAL)
+
+ help_text = wx.StaticText(
+ self,
+ wx.ID_ANY,
+ _("This extension fills shapes with a tartan (or tartan like) pattern."),
+ style=wx.ALIGN_LEFT
+ )
+ help_text.Wrap(500)
+ help_sizer.Add(help_text, 0, wx.ALL, 8)
+
+ help_sizer.Add((20, 20), 0, 0, 0)
+
+ website_info = wx.StaticText(self, wx.ID_ANY, _("More information on our website:"))
+ help_sizer.Add(website_info, 0, wx.ALL, 8)
+
+ website_link = wx.adv.HyperlinkCtrl(
+ self,
+ wx.ID_ANY,
+ _("https://inkstitch.org/docs/stitches/tartan-fill"),
+ _("https://inkstitch.org/docs/stitches/tartan-fill")
+ )
+ website_link.Bind(wx.adv.EVT_HYPERLINK, self.on_link_clicked)
+ help_sizer.Add(website_link, 0, wx.ALL, 8)
+
+ self.SetSizer(help_sizer)
+
+ def on_link_clicked(self, event):
+ event.Skip()
diff --git a/lib/gui/tartan/main_panel.py b/lib/gui/tartan/main_panel.py
new file mode 100644
index 00000000..238c8901
--- /dev/null
+++ b/lib/gui/tartan/main_panel.py
@@ -0,0 +1,271 @@
+# Authors: see git history
+#
+# Copyright (c) 2023 Authors
+# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
+
+import json
+from copy import copy
+
+import inkex
+import wx
+import wx.adv
+
+from ...elements import FillStitch, nodes_to_elements
+from ...exceptions import InkstitchException, format_uncaught_exception
+from ...i18n import _
+from ...stitch_plan import stitch_groups_to_stitch_plan
+from ...svg.tags import INKSTITCH_TARTAN
+from ...tartan.fill_element import prepare_tartan_fill_element
+from ...tartan.palette import Palette
+from ...tartan.svg import TartanSvgGroup
+from ...utils import DotDict
+from ...utils.threading import ExitThread, check_stop_flag
+from .. import PresetsPanel, PreviewRenderer, WarningPanel
+from . import CodePanel, CustomizePanel, EmbroideryPanel, HelpPanel
+
+
+class TartanMainPanel(wx.Panel):
+
+ def __init__(self, parent, simulator, elements, on_cancel=None, metadata=None, output_groups=inkex.Group()):
+ self.parent = parent
+ self.simulator = simulator
+ self.elements = elements
+ self.cancel_hook = on_cancel
+ self.palette = Palette()
+ self.metadata = metadata or dict()
+ self.output_groups = output_groups
+
+ super().__init__(parent, wx.ID_ANY)
+
+ self.SetWindowStyle(wx.FRAME_FLOAT_ON_PARENT | wx.DEFAULT_FRAME_STYLE)
+
+ # preview
+ self.preview_renderer = PreviewRenderer(self.render_stitch_plan, self.on_stitch_plan_rendered)
+ self.presets_panel = PresetsPanel(self)
+ # warnings
+ self.warning_panel = WarningPanel(self)
+ self.warning_panel.Hide()
+ # notebook
+ self.notebook_sizer = wx.BoxSizer(wx.VERTICAL)
+ self.notebook = wx.Notebook(self, wx.ID_ANY)
+ self.notebook_sizer.Add(self.warning_panel, 0, wx.EXPAND | wx.ALL, 10)
+ self.notebook_sizer.Add(self.notebook, 1, wx.EXPAND, 0)
+ # customize
+ self.customize_panel = CustomizePanel(self.notebook, self)
+ self.notebook.AddPage(self.customize_panel, _('Customize'))
+ self.customize_panel.SetupScrolling() # scroll_x=False)
+ # code
+ self.code_panel = CodePanel(self.notebook, self)
+ self.notebook.AddPage(self.code_panel, _("Palette Code"))
+ # embroidery settings
+ self.embroidery_panel = EmbroideryPanel(self.notebook, self)
+ self.notebook.AddPage(self.embroidery_panel, _("Embroidery Settings"))
+ # help
+ help_panel = HelpPanel(self.notebook)
+ self.notebook.AddPage(help_panel, _("Help"))
+ # apply and cancel buttons
+ apply_sizer = wx.BoxSizer(wx.HORIZONTAL)
+ self.cancel_button = wx.Button(self, label=_("Cancel"))
+ self.cancel_button.Bind(wx.EVT_BUTTON, self.cancel)
+ self.apply_button = wx.Button(self, label=_("Apply"))
+ self.apply_button.Bind(wx.EVT_BUTTON, self.apply)
+ apply_sizer.Add(self.cancel_button, 0, wx.RIGHT | wx.BOTTOM, 5)
+ apply_sizer.Add(self.apply_button, 0, wx.RIGHT | wx.BOTTOM, 10)
+
+ self.notebook_sizer.Add(self.presets_panel, 0, wx.EXPAND | wx.ALL, 10)
+ self.notebook_sizer.Add(apply_sizer, 0, wx.ALIGN_RIGHT | wx.ALL, 10)
+
+ self.SetSizer(self.notebook_sizer)
+
+ self.load_settings()
+ self.apply_settings()
+
+ self.Layout()
+ self.SetMinSize(self.notebook_sizer.CalcMin())
+
+ def update_from_code(self):
+ self.palette.update_from_code(self.code_panel.threadcount_text.GetValue())
+ self.customize_panel.symmetry_checkbox.SetValue(self.palette.symmetry)
+ self.customize_panel.warp_weft_checkbox.SetValue(self.palette.equal_warp_weft)
+ self.code_panel.threadcount_text.SetValue(self.palette.palette_code)
+ self.customize_panel.update_stripes(self.palette.palette_stripes)
+ self.customize_panel.update_symmetry()
+ self.customize_panel.update_warp_weft()
+ self.customize_panel.FitInside()
+ self.update_preview()
+
+ def update_from_stripes(self):
+ sizers = [self.customize_panel.warp_sizer]
+ if not self.customize_panel.warp_weft_checkbox.GetValue():
+ sizers.append(self.customize_panel.weft_sizer)
+ self.palette.update_from_stripe_sizer(
+ sizers,
+ self.customize_panel.symmetry_checkbox.GetValue(),
+ self.customize_panel.warp_weft_checkbox.GetValue()
+ )
+ self.update_code_text()
+ self.update_preview()
+
+ def update_code_text(self):
+ self.code_panel.threadcount_text.SetValue(self.palette.palette_code)
+ self.settings['palette'] = self.palette.palette_code
+
+ def load_settings(self):
+ """Load the settings saved into the SVG element"""
+ self.settings = DotDict({
+ "symmetry": True,
+ "equal_warp_weft": True,
+ "rotate": 0.0,
+ "scale": 100,
+ "offset_x": 0.0,
+ "offset_y": 0.0,
+ "palette": "K/10 W/?10",
+ "output": "embroidery",
+ "stitch_type": "legacy_fill",
+ "row_spacing": 1.0,
+ "angle_warp": 0.0,
+ "angle_weft": 90.0,
+ "min_stripe_width": 1.0,
+ "bean_stitch_repeats": 0
+ })
+
+ try:
+ self.settings.update(json.loads(self.elements[0].get(INKSTITCH_TARTAN)))
+ except (TypeError, ValueError, IndexError):
+ pass
+
+ def apply_settings(self):
+ """Make the settings in self.settings visible in the UI."""
+ self.customize_panel.rotate.SetValue(self.settings.rotate)
+ self.customize_panel.scale.SetValue(int(self.settings.scale))
+ self.customize_panel.offset_x.SetValue(self.settings.offset_x)
+ self.customize_panel.offset_y.SetValue(self.settings.offset_y)
+ self.code_panel.threadcount_text.SetValue(self.settings.palette)
+ self.embroidery_panel.set_output(self.settings.output)
+ self.embroidery_panel.set_stitch_type(self.settings.stitch_type)
+ self.embroidery_panel.svg_row_spacing.SetValue(self.settings.row_spacing)
+ self.embroidery_panel.angle_warp.SetValue(self.settings.angle_warp)
+ self.embroidery_panel.angle_weft.SetValue(self.settings.angle_weft)
+ self.embroidery_panel.min_stripe_width.SetValue(self.settings.min_stripe_width)
+ self.embroidery_panel.svg_bean_stitch_repeats.SetValue(self.settings.bean_stitch_repeats)
+ self.embroidery_panel.stitch_angle.SetValue(self.elements[0].get('inkstitch:tartan_angle', -45))
+ self.embroidery_panel.rows_per_thread.SetValue(self.elements[0].get('inkstitch:rows_per_thread', 2))
+ self.embroidery_panel.row_spacing.SetValue(self.elements[0].get('inkstitch:row_spacing_mm', 0.25))
+ underlay = self.elements[0].get('inkstitch:fill_underlay', "True").lower() in ('yes', 'y', 'true', 't', '1')
+ self.embroidery_panel.underlay.SetValue(underlay)
+ self.embroidery_panel.herringbone.SetValue(self.elements[0].get('inkstitch:herringbone_width_mm', 0))
+ self.embroidery_panel.bean_stitch_repeats.SetValue(self.elements[0].get('inkstitch:bean_stitch_repeats', '0'))
+
+ self.update_from_code()
+
+ self.customize_panel.symmetry_checkbox.SetValue(bool(self.settings.symmetry))
+ self.palette.update_symmetry(self.settings.symmetry)
+ self.customize_panel.warp_weft_checkbox.SetValue(bool(self.settings.equal_warp_weft))
+ self.customize_panel.update_warp_weft()
+
+ def save_settings(self):
+ """Save the settings into the SVG elements."""
+ for element in self.elements:
+ element.set(INKSTITCH_TARTAN, json.dumps(self.settings))
+
+ def get_preset_data(self):
+ # called by self.presets_panel
+ settings = dict(self.settings)
+ return settings
+
+ def _hide_warning(self):
+ self.warning_panel.clear()
+ self.warning_panel.Hide()
+ self.Layout()
+
+ def _show_warning(self, warning_text):
+ self.warning_panel.set_warning_text(warning_text)
+ self.warning_panel.Show()
+ self.Layout()
+
+ def update_preview(self, event=None):
+ self.preview_renderer.update()
+
+ def apply_preset_data(self, preset_data):
+ settings = DotDict(preset_data)
+ self.settings = settings
+ self.apply_settings()
+
+ def get_preset_suite_name(self):
+ # called by self.presets_panel
+ return "tartan"
+
+ def close(self):
+ self.GetTopLevelParent().Close()
+
+ def cancel(self, event):
+ if self.cancel_hook:
+ self.cancel_hook()
+ self.close()
+
+ def apply(self, event):
+ self.update_tartan()
+ self.save_settings()
+ self.close()
+
+ def render_stitch_plan(self):
+ if self.settings['output'] == 'svg':
+ self.update_tartan()
+ stitch_groups = self._get_svg_stitch_groups()
+ else:
+ self.save_settings()
+ stitch_groups = []
+ previous_stitch_group = None
+ for element in self.elements:
+ try:
+ # copy the embroidery element to drop the cache
+ stitch_groups.extend(copy(FillStitch(element)).embroider(previous_stitch_group))
+ if stitch_groups:
+ previous_stitch_group = stitch_groups[-1]
+ except (SystemExit, ExitThread):
+ raise
+ except InkstitchException as exc:
+ wx.CallAfter(self._show_warning, str(exc))
+ except Exception:
+ wx.CallAfter(self._show_warning, format_uncaught_exception())
+
+ if stitch_groups:
+ return stitch_groups_to_stitch_plan(
+ stitch_groups,
+ collapse_len=self.metadata['collapse_len_mm'],
+ min_stitch_len=self.metadata['min_stitch_len_mm']
+ )
+
+ def _get_svg_stitch_groups(self):
+ stitch_groups = []
+ previous_stitch_group = None
+ for element in self.elements:
+ parent = element.getparent()
+ embroidery_elements = nodes_to_elements(parent.iterdescendants())
+ for embroidery_element in embroidery_elements:
+ check_stop_flag()
+ if embroidery_element.node == element:
+ continue
+ try:
+ # copy the embroidery element to drop the cache
+ stitch_groups.extend(copy(embroidery_element).embroider(previous_stitch_group))
+ if stitch_groups:
+ previous_stitch_group = stitch_groups[-1]
+ except InkstitchException:
+ pass
+ except Exception:
+ pass
+ return stitch_groups
+
+ def update_tartan(self):
+ for element in self.elements:
+ check_stop_flag()
+ if self.settings['output'] == 'svg':
+ TartanSvgGroup(self.settings).generate(element)
+ else:
+ prepare_tartan_fill_element(element)
+
+ def on_stitch_plan_rendered(self, stitch_plan):
+ self.simulator.stop()
+ self.simulator.load(stitch_plan)
+ self.simulator.go()