Add multicolor satin extension (#2863)

pull/2859/head
Kaalleen 2024-05-01 06:59:46 +02:00 zatwierdzone przez GitHub
rodzic b218aac86c
commit 0c825d2163
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: B5690EEEBB952194
8 zmienionych plików z 591 dodań i 3 usunięć

Wyświetl plik

@ -48,6 +48,7 @@ from .preferences import Preferences
from .print_pdf import Print
from .remove_embroidery_settings import RemoveEmbroiderySettings
from .reorder import Reorder
from .satin_multicolor import SatinMulticolor
from .select_elements import SelectElements
from .selection_to_guide_line import SelectionToGuideLine
from .selection_to_pattern import SelectionToPattern
@ -108,6 +109,7 @@ __all__ = extensions = [ApplyPalette,
Print,
RemoveEmbroiderySettings,
Reorder,
SatinMulticolor,
SelectElements,
SelectionToGuideLine,
SelectionToPattern,

Wyświetl plik

@ -0,0 +1,56 @@
# 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 sys
import wx
import wx.adv
from inkex import errormsg
from ..elements import SatinColumn
from ..gui import MultiColorSatinPanel
from ..gui.simulator import SplitSimulatorWindow
from ..i18n import _
from ..utils.svg_data import get_pagecolor
from .base import InkstitchExtension
class SatinMulticolor(InkstitchExtension):
def __init__(self, *args, **kwargs):
self.elements = set()
self.cancelled = False
InkstitchExtension.__init__(self, *args, **kwargs)
def cancel(self):
self.cancelled = True
def effect(self):
self.get_elements()
satins = [element for element in self.elements if isinstance(element, SatinColumn)]
if not satins:
errormsg(_("Please select at least one satin column."))
return
metadata = self.get_inkstitch_metadata()
background_color = get_pagecolor(self.svg.namedview)
app = wx.App()
frame = SplitSimulatorWindow(
title=_("Ink/Stitch Multicolor Satin"),
panel_class=MultiColorSatinPanel,
elements=satins,
on_cancel=self.cancel,
metadata=metadata,
background_color=background_color,
target_duration=1
)
frame.Show()
app.MainLoop()
if self.cancelled:
# This prevents the superclass from outputting the SVG, because we
# may have modified the DOM.
sys.exit(0)

Wyświetl plik

@ -8,3 +8,4 @@ from .electron import open_url
from .presets import PresetsPanel
from .simulator import PreviewRenderer
from .warnings import WarningPanel
from .satin_multicolor.main_panel import MultiColorSatinPanel

Wyświetl plik

@ -0,0 +1,273 @@
# 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 time import time
import wx
from wx.lib.scrolledpanel import ScrolledPanel
from ...i18n import _
class ColorizePanel(ScrolledPanel):
def __init__(self, parent, panel):
self.panel = panel
ScrolledPanel.__init__(self, parent)
self.colorize_sizer = wx.BoxSizer(wx.VERTICAL)
general_settings_sizer = wx.FlexGridSizer(8, 2, 10, 20)
color_header_sizer = wx.BoxSizer(wx.HORIZONTAL)
self.color_sizer = wx.BoxSizer(wx.VERTICAL)
# general settings
general_settings_headline = wx.StaticText(self, label=_("General Settings"))
general_settings_headline.SetFont(wx.Font().Bold())
equististance_label = wx.StaticText(self, label=_("Equidistant colors"))
equististance_label.SetToolTip(_("Whether colors should be equidistant or have varying widths."))
self.equististance = wx.CheckBox(self)
self.equististance.SetValue(True)
self.equististance.Bind(wx.EVT_CHECKBOX, self._on_update_equidistance)
self.monochrome_width_label = wx.StaticText(self, label=_("Monochrome color width"))
self.monochrome_width_label.SetToolTip(_("Adapt color width here when equidistance is enabled."))
self.monochrome_width = wx.SpinCtrlDouble(self, min=0, max=100, initial=100, inc=1, style=wx.SP_WRAP)
self.monochrome_width.SetDigits(2)
self.monochrome_width.Bind(wx.EVT_SPINCTRLDOUBLE, self._on_update_monochrome_width)
overflow_left_label = wx.StaticText(self, label=_("Overflow left"))
self.overflow_left = wx.SpinCtrlDouble(self, min=0, max=100, initial=0, inc=0.1, style=wx.SP_WRAP)
self.overflow_left.SetDigits(2)
self.overflow_left.Bind(wx.EVT_SPINCTRLDOUBLE, self._update)
overflow_right_label = wx.StaticText(self, label=_("Overflow right"))
self.overflow_right = wx.SpinCtrlDouble(self, min=0, max=100, initial=0, inc=0.1, style=wx.SP_WRAP)
self.overflow_right.SetDigits(2)
self.overflow_right.Bind(wx.EVT_SPINCTRLDOUBLE, self._update)
pull_compensation_label = wx.StaticText(self, label=_("Pull compensation (mm)"))
self.pull_compensation = wx.SpinCtrlDouble(self, min=0, max=100, initial=0, inc=0.1, style=wx.SP_WRAP)
self.pull_compensation.SetDigits(2)
self.pull_compensation.Bind(wx.EVT_SPINCTRLDOUBLE, self._update)
seed_label = wx.StaticText(self, label=_("Random seed"))
self.seed = wx.TextCtrl(self)
self.seed.SetValue(str(time()))
self.seed.Bind(wx.EVT_TEXT, self._update)
# embroidery settings
keep_original_label = wx.StaticText(self, label=_("Keep original satin"))
self.keep_original = wx.CheckBox(self)
self.keep_original.SetValue(True)
# Colors
color_settings_headline = wx.StaticText(self, label=_("Colors"))
color_settings_headline.SetFont(wx.Font().Bold())
self.total_width = wx.StaticText(self)
self.total_width.SetToolTip(_("Overflow excluded"))
self.add_color_button = wx.Button(self, label=_("Add"))
self.add_color_button.Bind(wx.EVT_BUTTON, self._add_color_event)
# Add to sizers
general_settings_sizer.Add(equististance_label, 0, wx.ALL, 0)
general_settings_sizer.Add(self.equististance, 0, wx.ALL | wx.EXPAND, 0)
general_settings_sizer.Add(self.monochrome_width_label, 0, wx.LEFT, 30)
general_settings_sizer.Add(self.monochrome_width, 0, wx.ALL | wx.EXPAND, 0)
general_settings_sizer.Add(overflow_left_label, 0, wx.ALL, 0)
general_settings_sizer.Add(self.overflow_left, 0, wx.ALL | wx.EXPAND, 0)
general_settings_sizer.Add(overflow_right_label, 0, wx.ALL, 0)
general_settings_sizer.Add(self.overflow_right, 0, wx.ALL | wx.EXPAND, 0)
general_settings_sizer.Add(pull_compensation_label, 0, wx.ALL, 0)
general_settings_sizer.Add(self.pull_compensation, 0, wx.ALL | wx.EXPAND, 0)
general_settings_sizer.Add(seed_label, 0, wx.ALL, 0)
general_settings_sizer.Add(self.seed, 0, wx.ALL | wx.EXPAND, 0)
general_settings_sizer.Add(keep_original_label, 0, wx.TOP, 30)
general_settings_sizer.Add(self.keep_original, 0, wx.TOP | wx.EXPAND, 30)
general_settings_sizer.AddGrowableCol(1)
color_header_sizer.Add(color_settings_headline, 0, wx.ALL, 10)
color_header_sizer.Add((0, 0), 1, wx.ALL | wx.EXPAND, 10)
color_header_sizer.Add(self.total_width, 0, wx.ALL, 10)
self.colorize_sizer.Add(wx.StaticLine(self), 0, wx.ALL | wx.EXPAND, 10)
self.colorize_sizer.Add(general_settings_headline, 0, wx.ALL, 10)
self.colorize_sizer.Add(general_settings_sizer, 0, wx.ALL | wx.EXPAND, 20)
self.colorize_sizer.Add(wx.StaticLine(self), 0, wx.ALL | wx.EXPAND, 10)
self.colorize_sizer.Add(color_header_sizer, 0, wx.EXPAND | wx.ALL, 10)
self.colorize_sizer.Add(self.color_sizer, 0, wx.EXPAND | wx.ALL, 10)
self.colorize_sizer.Add(self.add_color_button, 0, wx.ALIGN_RIGHT | wx.ALL, 10)
self.SetSizer(self.colorize_sizer)
def _on_update_monochrome_width(self, event):
equidistance = self.equististance.GetValue()
if not equidistance:
return
width = self.monochrome_width.GetValue()
num_colors = len(self.color_sizer.GetChildren())
margin = (100 - width * num_colors) / max(num_colors - 1, 1)
self._set_widget_width_value(width, margin)
self._update()
def _add_color_event(self, event):
self.add_color()
def add_color(self, color='black'):
colorsizer = wx.BoxSizer(wx.HORIZONTAL)
position = wx.Button(self, label='', style=wx.BU_EXACTFIT)
position.SetToolTip(_("Click to move color up."))
position.Bind(wx.EVT_BUTTON, self._move_color_up)
colorpicker = wx.ColourPickerCtrl(self, colour=wx.Colour(color))
colorpicker.SetToolTip(_("Select color"))
colorpicker.Bind(wx.EVT_COLOURPICKER_CHANGED, self._update)
color_width = wx.SpinCtrlDouble(self, min=0, max=100, initial=0, style=wx.SP_WRAP)
color_width.SetDigits(2)
color_width.SetToolTip(_("Monochrome width. Can be changed individually when equidistance is disabled."))
color_width.Bind(wx.EVT_SPINCTRLDOUBLE, self._update)
color_margin_right = wx.SpinCtrlDouble(self, min=0, max=100, initial=0, style=wx.SP_WRAP)
color_margin_right.SetDigits(2)
color_margin_right.SetToolTip(_("Margin right (bicolor section). Can be changed individually when equidistance is disabled."))
color_margin_right.Bind(wx.EVT_SPINCTRLDOUBLE, self._update)
remove_button = wx.Button(self, label='X')
remove_button.SetToolTip(_("Remove color"))
remove_button.Bind(wx.EVT_BUTTON, self._remove_color)
colorsizer.Add(position, 0, wx.CENTER | wx.RIGHT | wx.TOP | wx.RESERVE_SPACE_EVEN_IF_HIDDEN, 5)
colorsizer.Add(colorpicker, 0, wx.RIGHT | wx.TOP, 5)
colorsizer.Add(color_width, 1, wx.RIGHT | wx.TOP, 5)
colorsizer.Add(color_margin_right, 1, wx.RIGHT | wx.TOP | wx.RESERVE_SPACE_EVEN_IF_HIDDEN, 5)
colorsizer.Add(remove_button, 0, wx.CENTER | wx.TOP, 5)
self.color_sizer.Add(colorsizer, 0, wx.EXPAND | wx.ALL, 10)
if self.equististance.GetValue():
color_margin_right.Enable(False)
color_width.Enable(False)
else:
color_margin_right.Enable(True)
color_width.Enable(True)
self._update_colors()
color_margin_right.Show(False)
if len(self.color_sizer.GetChildren()) > 1:
self.color_sizer.GetChildren()[-2].GetSizer().GetChildren()[3].GetWindow().Show()
self._update()
self.FitInside()
self.Layout()
def _move_color_up(self, event):
color = event.GetEventObject()
sizer = color.GetContainingSizer()
main_sizer = self.color_sizer
for i, item in enumerate(main_sizer.GetChildren()):
if item.GetSizer() == sizer:
index = i
break
if index == len(main_sizer.GetChildren()) - 1:
last_sizer = main_sizer.GetChildren()[-2].GetSizer().GetChildren()
last_sizer[2].GetWindow().Show(False)
sizer.GetChildren()[2].GetWindow().Show()
index = max(0, (index - 1))
if index == 0:
previous_first = main_sizer.GetChildren()[0].GetSizer().GetChildren()
previous_first[0].GetWindow().Show()
sizer.GetChildren()[0].GetWindow().Show(False)
main_sizer.Detach(sizer)
main_sizer.Insert(index, sizer, 0, wx.EXPAND | wx.ALL, 10)
self.FitInside()
self._update()
self.Layout()
def _remove_color(self, event):
sizer = event.GetEventObject().GetContainingSizer()
sizer.Clear(True)
self.color_sizer.Remove(sizer)
self.FitInside()
self._update_colors()
self._update()
def _on_update_equidistance(self, event=None):
if self.equististance.GetValue():
self.monochrome_width_label.Enable(True)
self.monochrome_width.Enable(True)
self._set_widget_status(False)
self._update_colors()
else:
self.monochrome_width_label.Enable(False)
self.monochrome_width.Enable(False)
self._set_widget_status(True)
self._update()
def _set_widget_status(self, status):
for color in self.color_sizer.GetChildren():
inner_sizer = color.GetSizer()
for color_widget in inner_sizer:
widget = color_widget.GetWindow()
if isinstance(widget, wx.SpinCtrlDouble):
widget.Enable(status)
def _set_widget_width_value(self, value, margin=0):
first = True
for color in self.color_sizer.GetChildren():
inner_sizer = color.GetSizer()
for color_widget in inner_sizer:
widget = color_widget.GetWindow()
if first and widget.Label == "":
inner_sizer.Hide(widget)
first = False
if isinstance(widget, wx.SpinCtrlDouble):
widget.SetValue(value)
widget.GetNextSibling().SetValue(margin)
break
def get_total_width(self):
width = 0
colors = self.color_sizer.GetChildren()
for color in colors:
inner_sizer = color.GetSizer()
for color_widget in inner_sizer:
widget = color_widget.GetWindow()
if isinstance(widget, wx.SpinCtrlDouble):
width += widget.GetValue()
last_margin = inner_sizer.GetChildren()[3].GetWindow().GetValue()
width -= last_margin
return round(width, 2)
def _update(self, event=None):
width = self.get_total_width()
self.total_width.SetLabel(_("Total width: {width}%").format(width=width))
if width > 100:
self.total_width.SetForegroundColour("red")
else:
self.total_width.SetForegroundColour(wx.NullColour)
self.panel.update_preview()
def _update_colors(self):
equidistance = self.equististance.GetValue()
num_colors = len(self.color_sizer.GetChildren())
if equidistance:
max_width = 100 / max(1, num_colors)
monochrome_value = self.monochrome_width.GetValue()
if monochrome_value > max_width:
self._set_widget_width_value(max_width)
else:
margin = (100 - monochrome_value * num_colors) / max(1, num_colors - 1)
self._set_widget_width_value(monochrome_value, margin)
self.monochrome_width.SetMax(max_width)
self.Refresh()
self._update()

Wyświetl plik

@ -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 simulates a multicolor satin by creating colored copies of the selected satin(s)."),
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/satin-tools/#multicolor-satin"),
_("https://inkstitch.org/docs/satin-tools/#multicolor-satin")
)
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()

Wyświetl plik

@ -0,0 +1,199 @@
# 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 copy import copy
import inkex
import wx
import wx.adv
from ...elements import SatinColumn
from ...exceptions import InkstitchException, format_uncaught_exception
from ...i18n import _
from ...stitch_plan import stitch_groups_to_stitch_plan
from ...utils.threading import ExitThread, check_stop_flag
from .. import PreviewRenderer, WarningPanel
from .colorize import ColorizePanel
from .help_panel import HelpPanel
class MultiColorSatinPanel(wx.Panel):
def __init__(self, parent, simulator, elements, on_cancel=None, metadata=None, background_color='white'):
self.parent = parent
self.simulator = simulator
self.elements = elements
self.cancel_hook = on_cancel
self.metadata = metadata or dict()
self.background_color = background_color
self.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)
# 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.colorize_panel = ColorizePanel(self.notebook, self)
self.notebook.AddPage(self.colorize_panel, _('Colorize'))
self.colorize_panel.SetupScrolling()
# 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(apply_sizer, 0, wx.ALIGN_RIGHT | wx.ALL, 10)
self.SetSizer(self.notebook_sizer)
self.colorize_panel.add_color(self.elements[0].color)
self.Layout()
self.SetMinSize(self.notebook_sizer.CalcMin())
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 close(self):
self.GetTopLevelParent().Close()
def cancel(self, event):
if self.cancel_hook:
self.cancel_hook()
self.close()
def apply(self, event):
self.update_satin_elements()
if not self.colorize_panel.keep_original.GetValue():
for element in self.elements:
element.node.getparent().remove(element.node)
self.close()
def render_stitch_plan(self):
self.update_satin_elements()
stitch_groups = self._get_stitch_groups()
if stitch_groups:
stitch_plan = stitch_groups_to_stitch_plan(
stitch_groups,
collapse_len=self.metadata['collapse_len_mm'],
min_stitch_len=self.metadata['min_stitch_len_mm']
)
return stitch_plan
def _get_stitch_groups(self):
stitch_groups = []
for element in self.satin_elements:
try:
# copy the embroidery element to drop the cache
stitch_group = copy(SatinColumn(element)).embroider(None)
stitch_groups.extend(stitch_group)
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())
return stitch_groups
def update_satin_elements(self):
# empty old groups
for group in self.output_groups:
group.getparent().remove(group)
self.output_groups = []
overflow_left = self.colorize_panel.overflow_left.GetValue()
overflow_right = self.colorize_panel.overflow_right.GetValue()
pull_compensation = self.colorize_panel.pull_compensation.GetValue()
seed = self.colorize_panel.seed.GetValue()
self.satin_elements = []
color_sizer = self.colorize_panel.color_sizer.GetChildren()
num_colors = len(color_sizer)
for element in self.elements:
check_stop_flag()
layer = element.node.getparent()
index = layer.index(element.node)
group = inkex.Group()
group.label = _("Multicolor Satin Group")
current_position = 0
previous_margin = overflow_left
for i, segment_sizer in enumerate(color_sizer):
segment = segment_sizer.GetSizer().GetChildren()
color = segment[1].GetWindow().GetColour().GetAsString(wx.C2S_HTML_SYNTAX)
if i == num_colors - 1:
margin = overflow_right
else:
margin = segment[3].GetWindow().GetValue()
width = segment[2].GetWindow().GetValue()
new_satin = copy(element.node)
new_satin.style['stroke'] = color
new_satin.set('inkstitch:pull_compensation_mm', pull_compensation)
new_satin.set('inkstitch:random_seed', seed)
reverse_rails = self._get_new_reverse_rails_param(element, i)
if reverse_rails is not None:
new_satin.set('inkstitch:reverse_rails', reverse_rails)
if i % 2 == 0:
new_satin.set('inkstitch:swap_satin_rails', False)
new_satin.set('inkstitch:random_width_increase_percent', f'{ margin } 0')
new_satin.set('inkstitch:random_width_decrease_percent', f'0 { -previous_margin }')
new_satin.set('inkstitch:pull_compensation_percent', f'{ current_position + width - 100} { -current_position }')
else:
new_satin.set('inkstitch:swap_satin_rails', True)
new_satin.set('inkstitch:random_width_increase_percent', f'0 { margin }')
new_satin.set('inkstitch:random_width_decrease_percent', f'{ -previous_margin } 0')
new_satin.set('inkstitch:pull_compensation_percent', f'{ -current_position } { current_position + width - 100}')
previous_margin = margin
current_position += width + margin
group.add(new_satin)
self.satin_elements.append(new_satin)
layer.insert(index + 1, group)
self.output_groups.append(group)
def _get_new_reverse_rails_param(self, element, i):
reverse_rails = element._get_rails_to_reverse()
if any(reverse_rails) and element.reverse_rails == 'automatic':
if (reverse_rails[0] and i % 2 == 0) or (reverse_rails[1] and i % 2 != 0):
return 'first'
else:
return 'second'
return None
def on_stitch_plan_rendered(self, stitch_plan):
self.simulator.stop()
self.simulator.load(stitch_plan)
self.simulator.go()

Wyświetl plik

@ -6,7 +6,6 @@
import json
from copy import copy
import inkex
import wx
import wx.adv
@ -26,7 +25,7 @@ from . import CodePanel, CustomizePanel, EmbroideryPanel, HelpPanel
class TartanMainPanel(wx.Panel):
def __init__(self, parent, simulator, elements, on_cancel=None, metadata=None, background_color='white', output_groups=inkex.Group()):
def __init__(self, parent, simulator, elements, on_cancel=None, metadata=None, background_color='white'):
self.parent = parent
self.simulator = simulator
self.elements = elements
@ -34,7 +33,6 @@ class TartanMainPanel(wx.Panel):
self.palette = Palette()
self.metadata = metadata or dict()
self.background_color = background_color
self.output_groups = output_groups
super().__init__(parent, wx.ID_ANY)

Wyświetl plik

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension translationdomain="inkstitch" xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Multicolor Satin</name>
<id>org.{{ id_inkstitch }}.satin_multicolor</id>
<param name="extension" type="string" gui-hidden="true">satin_multicolor</param>
<effect needs-live-preview="false">
<object-type>all</object-type>
<effects-menu>
<submenu name="{{ menu_inkstitch }}" translatable="no">
<submenu name="Tools: Satin" />
</submenu>
</effects-menu>
</effect>
<script>
{{ command_tag | safe }}
</script>
</inkscape-extension>