diff options
Diffstat (limited to 'lib/gui')
| -rw-r--r-- | lib/gui/tartan/__init__.py | 10 | ||||
| -rw-r--r-- | lib/gui/tartan/code_panel.py | 59 | ||||
| -rw-r--r-- | lib/gui/tartan/customize_panel.py | 300 | ||||
| -rw-r--r-- | lib/gui/tartan/embroidery_panel.py | 201 | ||||
| -rw-r--r-- | lib/gui/tartan/help_panel.py | 42 | ||||
| -rw-r--r-- | lib/gui/tartan/main_panel.py | 271 |
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() |
