attach params/lettering simulator window and allow detach (#2557)

pull/2568/head
Lex Neva 2023-10-21 12:16:34 -04:00 zatwierdzone przez GitHub
rodzic 526cd48a4f
commit 82f2edac1a
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
11 zmienionych plików z 364 dodań i 260 usunięć

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 2.8 KiB

Wyświetl plik

@ -0,0 +1,98 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="256"
height="256"
viewBox="0 0 256 256"
id="svg8375"
version="1.1"
inkscape:version="1.3 (1:1.3+202307231459+0e150ed6c4)"
sodipodi:docname="detach_window.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs8377">
<marker
style="overflow:visible"
id="Triangle"
refX="0"
refY="0"
orient="auto-start-reverse"
inkscape:stockid="Triangle arrow"
markerWidth="0.5"
markerHeight="0.5"
viewBox="0 0 1 1"
inkscape:isstock="true"
inkscape:collect="always"
preserveAspectRatio="xMidYMid">
<path
transform="scale(0.5)"
style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:1pt"
d="M 5.77,0 -2.88,5 V -5 Z"
id="path135" />
</marker>
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="1.3183809"
inkscape:cx="193.41906"
inkscape:cy="284.81905"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
units="mm"
inkscape:window-width="1366"
inkscape:window-height="705"
inkscape:window-x="-4"
inkscape:window-y="-4"
inkscape:window-maximized="1"
inkscape:showpageshadow="2"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1" />
<metadata
id="metadata8380">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<rect
style="fill:#000000;stroke-width:22.3274"
id="rect1-3-7"
width="94.246475"
height="86.769928"
x="159.3401"
y="3.0987689" />
<rect
style="fill:#000000;stroke-width:13.626"
id="rect1-3-7-5"
width="57.516991"
height="52.954185"
x="3.5270462"
y="199.60167" />
<path
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:17.0079;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Triangle)"
d="M 69.827955,184.17307 126.75642,123.7445"
id="path3"
sodipodi:nodetypes="cc" />
</g>
</svg>

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 2.8 KiB

Wyświetl plik

@ -14,36 +14,37 @@ import wx
import wx.adv
import wx.lib.agw.floatspin as fs
from .commands import CommandsExtension
from .lettering_custom_font_dir import get_custom_font_dir
from ..elements import nodes_to_elements
from ..gui import PresetsPanel, SimulatorPreview, info_dialog
from ..gui import PresetsPanel, PreviewRenderer, info_dialog
from ..gui.simulator import SplitSimulatorWindow
from ..i18n import _
from ..lettering import Font, FontError
from ..lettering.categories import FONT_CATEGORIES, FontCategory
from ..stitch_plan import stitch_groups_to_stitch_plan
from ..svg import get_correction_transform
from ..svg.tags import (INKSCAPE_LABEL, INKSTITCH_LETTERING, SVG_GROUP_TAG,
SVG_PATH_TAG)
from ..utils import DotDict, cache, get_bundled_dir, get_resource_dir
from ..utils.threading import ExitThread
from .commands import CommandsExtension
from .lettering_custom_font_dir import get_custom_font_dir
from ..utils import DotDict, cache, get_bundled_dir
from ..utils.threading import ExitThread, check_stop_flag
class LetteringFrame(wx.Frame):
class LetteringPanel(wx.Panel):
DEFAULT_FONT = "small_font"
def __init__(self, *args, **kwargs):
self.group = kwargs.pop('group')
self.cancel_hook = kwargs.pop('on_cancel', None)
self.metadata = kwargs.pop('metadata', [])
def __init__(self, parent, simulator, group, on_cancel=None, metadata=None):
self.parent = parent
self.simulator = simulator
self.group = group
self.cancel_hook = on_cancel
self.metadata = metadata or dict()
# begin wxGlade: MyFrame.__init__
wx.Frame.__init__(self, None, wx.ID_ANY, _("Ink/Stitch Lettering"))
self.SetWindowStyle(wx.FRAME_FLOAT_ON_PARENT)
super().__init__(parent, wx.ID_ANY)
icon = wx.Icon(os.path.join(get_resource_dir("icons"), "inkstitch256x256.png"))
self.SetIcon(icon)
self.SetWindowStyle(wx.FRAME_FLOAT_ON_PARENT | wx.DEFAULT_FRAME_STYLE)
self.preview = SimulatorPreview(self, target_duration=1)
self.preview_renderer = PreviewRenderer(self.render_stitch_plan, self.on_stitch_plan_rendered)
self.presets_panel = PresetsPanel(self)
# font
@ -257,11 +258,11 @@ class LetteringFrame(wx.Frame):
self.settings[attribute] = event.GetEventObject().GetValue()
if attribute == "text" and self.font_glyph_filter.GetValue() is True:
self.on_filter_changed()
self.preview.update()
self.preview_renderer.update()
def on_trim_option_change(self, event=None):
self.settings.trim_option = self.trim_option_choice.GetCurrentSelection()
self.preview.update()
self.preview_renderer.update()
def on_font_changed(self, event=None):
font = self.fonts.get(self.font_chooser.GetValue(), self.default_font)
@ -329,7 +330,7 @@ class LetteringFrame(wx.Frame):
self.Layout()
def update_preview(self, event=None):
self.preview.update()
self.preview_renderer.update()
def update_lettering(self, raise_error=False):
# return if there is no font in the font list (possibly due to a font size filter)
@ -367,19 +368,24 @@ class LetteringFrame(wx.Frame):
if self.settings.scale != 100 and not destination_group.get('transform', None):
destination_group.attrib['transform'] = 'scale(%s)' % (self.settings.scale / 100.0)
def generate_patches(self, abort_early=None):
patches = []
def render_stitch_plan(self):
stitch_groups = []
try:
self.update_lettering()
elements = nodes_to_elements(self.group.iterdescendants(SVG_PATH_TAG))
for element in elements:
if abort_early and abort_early.is_set():
# cancel; settings were updated and we need to start over
return []
check_stop_flag()
patches.extend(element.embroider(None))
stitch_groups.extend(element.embroider(None))
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']
)
except SystemExit:
raise
except ExitThread:
@ -390,7 +396,10 @@ class LetteringFrame(wx.Frame):
# satins or division by zero caused by incorrect param values.
pass
return patches
def on_stitch_plan_rendered(self, stitch_plan):
self.simulator.stop()
self.simulator.load(stitch_plan)
self.simulator.go()
def get_preset_data(self):
# called by self.presets_panel
@ -409,14 +418,12 @@ class LetteringFrame(wx.Frame):
return "lettering"
def apply(self, event):
self.preview.disable()
self.update_lettering(True)
self.save_settings()
self.close()
def close(self):
self.preview.close()
self.Destroy()
self.GetTopLevelParent().Close()
def cancel(self, event):
if self.cancel_hook:
@ -534,14 +541,14 @@ class Lettering(CommandsExtension):
def effect(self):
metadata = self.get_inkstitch_metadata()
app = wx.App()
frame = LetteringFrame(group=self.get_or_create_group(), on_cancel=self.cancel, metadata=metadata)
# position left, center
current_screen = wx.Display.GetFromPoint(wx.GetMousePosition())
display = wx.Display(current_screen)
display_size = display.GetClientArea()
frame_size = frame.GetSize()
frame.SetPosition((int(display_size[0]), int(display_size[3] / 2 - frame_size[1] / 2)))
frame = SplitSimulatorWindow(
title=_("Ink/Stitch Lettering"),
panel_class=LetteringPanel,
group=self.get_or_create_group(),
on_cancel=self.cancel,
metadata=metadata,
target_duration=1
)
frame.Show()
app.MainLoop()

Wyświetl plik

@ -15,18 +15,20 @@ from secrets import randbelow
import wx
from wx.lib.scrolledpanel import ScrolledPanel
from .base import InkstitchExtension
from ..commands import is_command, is_command_symbol
from ..elements import (Clone, EmbroideryElement, FillStitch, Polyline,
SatinColumn, Stroke)
from ..elements.clone import is_clone
from ..exceptions import InkstitchException, format_uncaught_exception
from ..gui import PresetsPanel, SimulatorPreview, WarningPanel
from ..gui import PresetsPanel, PreviewRenderer, WarningPanel
from ..gui.simulator import SplitSimulatorWindow
from ..i18n import _
from ..stitch_plan import stitch_groups_to_stitch_plan
from ..svg.tags import SVG_POLYLINE_TAG
from ..utils import get_resource_dir
from ..utils.param import ParamOption
from ..utils.threading import ExitThread, check_stop_flag
from .base import InkstitchExtension
def grouper(iterable_obj, count, fillvalue=None):
@ -439,7 +441,7 @@ class ParamsTab(ScrolledPanel):
for item in param.select_items:
self.choice_widgets[item].extend([input, col4])
self.settings_grid.Add(input, flag=wx.ALIGN_CENTER_VERTICAL | wx.LEFT | wx.EXPAND, border=40)
self.settings_grid.Add(input, flag=wx.ALIGN_CENTER_VERTICAL | wx.LEFT | wx.EXPAND, border=10)
self.settings_grid.Add(col4, flag=wx.ALIGN_CENTER_VERTICAL)
self.inputs_to_params = {v: k for k, v in self.param_inputs.items()}
@ -470,20 +472,17 @@ class ParamsTab(ScrolledPanel):
# end of class SatinPane
class SettingsFrame(wx.Frame):
def __init__(self, *args, **kwargs):
self.tabs_factory = kwargs.pop('tabs_factory', [])
self.cancel_hook = kwargs.pop('on_cancel', None)
self.metadata = kwargs.pop('metadata', [])
class SettingsPanel(wx.Panel):
def __init__(self, parent, tabs_factory=None, on_cancel=None, metadata=None, simulator=None):
self.tabs_factory = tabs_factory
self.cancel_hook = on_cancel
self.metadata = metadata
self.simulator = simulator
self.parent = parent
# begin wxGlade: MyFrame.__init__
wx.Frame.__init__(self, None, wx.ID_ANY, _("Embroidery Params"))
super().__init__(self.parent, wx.ID_ANY)
self.SetWindowStyle(wx.FRAME_FLOAT_ON_PARENT)
icon = wx.Icon(os.path.join(
get_resource_dir("icons"), "inkstitch256x256.png"))
self.SetIcon(icon)
self.preview_renderer = PreviewRenderer(self.render_stitch_plan, self.on_stitch_plan_rendered)
self.notebook = wx.Notebook(self, wx.ID_ANY)
self.tabs = self.tabs_factory(self.notebook)
@ -491,7 +490,6 @@ class SettingsFrame(wx.Frame):
for tab in self.tabs:
tab.on_change(self.update_preview)
self.preview = SimulatorPreview(self)
self.presets_panel = PresetsPanel(self)
self.warning_panel = WarningPanel(self)
self.warning_panel.Hide()
@ -507,43 +505,45 @@ class SettingsFrame(wx.Frame):
self.apply_button = wx.Button(self, wx.ID_ANY, _("Apply and Quit"))
self.apply_button.Bind(wx.EVT_BUTTON, self.apply)
self.notebook.SetMinSize((800, 600))
self.__do_layout()
# end wxGlade
self.update_preview()
def update_preview(self, tab):
self.preview.update()
def update_preview(self, tab=None):
self.simulator.stop()
self.simulator.clear()
self.preview_renderer.update()
def generate_patches(self, abort_early):
# called by self.preview
patches = []
def render_stitch_plan(self):
stitch_groups = []
nodes = []
for tab in self.tabs:
tab.apply()
if tab.enabled() and not tab.is_dependent_tab():
nodes.extend(tab.nodes)
check_stop_flag()
# sort nodes into the proper stacking order
nodes.sort(key=lambda node: node.order)
try:
wx.CallAfter(self._hide_warning)
for node in nodes:
if abort_early.is_set():
# cancel; params were updated and we need to start over
return []
# Making a copy of the embroidery element is an easy
# way to drop the cache in the @cache decorators used
# for many params in embroider.py.
patches.extend(copy(node).embroider(None))
stitch_groups.extend(copy(node).embroider(None))
check_stop_flag()
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']
)
except (SystemExit, ExitThread):
raise
except InkstitchException as exc:
@ -551,7 +551,10 @@ class SettingsFrame(wx.Frame):
except Exception:
wx.CallAfter(self._show_warning, format_uncaught_exception())
return patches
def on_stitch_plan_rendered(self, stitch_plan):
self.simulator.stop()
self.simulator.load(stitch_plan)
self.simulator.go()
def _hide_warning(self):
self.warning_panel.clear()
@ -589,7 +592,7 @@ class SettingsFrame(wx.Frame):
for tab in self.tabs:
tab.load_preset(preset_data)
self.preview.update()
self.preview_renderer.update()
def _apply(self):
for tab in self.tabs:
@ -601,13 +604,11 @@ class SettingsFrame(wx.Frame):
self.close()
def use_last(self, event):
self.preview.disable()
self.presets_panel.load_preset("__LAST__")
self.apply(event)
def close(self):
self.preview.close()
self.Destroy()
self.GetTopLevelParent().Close()
def cancel(self, event):
if self.cancel_hook:
@ -781,18 +782,14 @@ class Params(InkstitchExtension):
try:
app = wx.App()
metadata = self.get_inkstitch_metadata()
frame = SettingsFrame(
frame = SplitSimulatorWindow(
title=_("Embroidery Params"),
panel_class=SettingsPanel,
tabs_factory=self.create_tabs,
on_cancel=self.cancel,
metadata=metadata)
# position left, center
current_screen = wx.Display.GetFromPoint(wx.GetMousePosition())
display = wx.Display(current_screen)
display_size = display.GetClientArea()
frame_size = frame.GetSize()
frame.SetPosition((int(display_size[0]), int(
display_size[3]/2 - frame_size[1]/2)))
metadata=metadata,
target_duration=5
)
frame.Show()
app.MainLoop()

Wyświetl plik

@ -6,5 +6,5 @@
from .dialogs import confirm_dialog, info_dialog
from .electron import open_url
from .presets import PresetsPanel
from .simulator import EmbroiderySimulator, SimulatorPreview, show_simulator
from .simulator import PreviewRenderer, show_simulator
from .warnings import WarningPanel

Wyświetl plik

@ -16,7 +16,7 @@ class ElementInfoFrame(wx.Frame):
self.index = 0
wx.Frame.__init__(self, None, wx.ID_ANY, _("Element Info"), *args, **kwargs)
self.SetWindowStyle(wx.FRAME_FLOAT_ON_PARENT)
self.SetWindowStyle(wx.FRAME_FLOAT_ON_PARENT | wx.DEFAULT_FRAME_STYLE)
self.main_panel = wx.Panel(self, wx.ID_ANY)

Wyświetl plik

@ -16,7 +16,7 @@ class PreferencesFrame(wx.Frame):
wx.Frame.__init__(self, None, wx.ID_ANY, _("Preferences"), *args, **kwargs)
self.SetTitle(_("Preferences"))
self.SetWindowStyle(wx.FRAME_FLOAT_ON_PARENT)
self.SetWindowStyle(wx.FRAME_FLOAT_ON_PARENT | wx.DEFAULT_FRAME_STYLE)
metadata = self.extension.get_inkstitch_metadata()

Wyświetl plik

@ -12,9 +12,10 @@ from wx.lib.intctrl import IntCtrl
from lib.debug import debug
from lib.utils import get_resource_dir
from lib.utils.settings import global_settings
from lib.utils.threading import ExitThread
from ..i18n import _
from ..stitch_plan import stitch_groups_to_stitch_plan, stitch_plan_from_file
from ..stitch_plan import stitch_plan_from_file
from ..svg import PIXELS_PER_MM
# L10N command label at bottom of simulator window
@ -34,7 +35,8 @@ class ControlPanel(wx.Panel):
def __init__(self, parent, *args, **kwargs):
""""""
self.parent = parent
self.stitch_plan = kwargs.pop('stitch_plan')
self.stitch_plan = kwargs.pop('stitch_plan', None)
self.detach_callback = kwargs.pop('detach_callback', None)
self.target_stitches_per_second = kwargs.pop('stitches_per_second')
self.target_duration = kwargs.pop('target_duration')
kwargs['style'] = wx.BORDER_SUNKEN
@ -97,16 +99,16 @@ class ControlPanel(wx.Panel):
self.btnNpp.Bind(wx.EVT_TOGGLEBUTTON, self.toggle_npp)
self.btnNpp.SetBitmap(self.load_icon('npp'))
self.btnNpp.SetToolTip(_('Display needle penetration point (O)'))
self.slider = SimulatorSlider(self, -1, value=1, minValue=1, maxValue=self.stitch_plan.num_stitches)
self.slider = SimulatorSlider(self, -1, value=1, minValue=1, maxValue=2)
self.slider.Bind(wx.EVT_SLIDER, self.on_slider)
self.stitchBox = IntCtrl(self, -1, value=1, min=1, max=self.stitch_plan.num_stitches,
size=((100, -1)), limited=True, allow_none=True, style=wx.TE_PROCESS_ENTER)
self.stitchBox = IntCtrl(self, -1, value=1, min=1, max=2, limited=True, allow_none=True,
size=((100, -1)), style=wx.TE_PROCESS_ENTER)
self.stitchBox.Bind(wx.EVT_LEFT_DOWN, self.on_stitch_box_focus)
self.stitchBox.Bind(wx.EVT_SET_FOCUS, self.on_stitch_box_focus)
self.stitchBox.Bind(wx.EVT_TEXT_ENTER, self.on_stitch_box_focusout)
self.stitchBox.Bind(wx.EVT_KILL_FOCUS, self.on_stitch_box_focusout)
self.Bind(wx.EVT_LEFT_DOWN, self.on_stitch_box_focusout)
self.totalstitchText = wx.StaticText(self, -1, label=f"/ { self.stitch_plan.num_stitches }")
self.totalstitchText = wx.StaticText(self, -1, label="/ ________")
self.btnJump = wx.BitmapToggleButton(self, -1, style=self.button_style)
self.btnJump.SetToolTip(_('Show jump stitches'))
self.btnJump.SetBitmap(self.load_icon('jump'))
@ -123,12 +125,17 @@ class ControlPanel(wx.Panel):
self.btnColorChange.SetToolTip(_('Show color changes'))
self.btnColorChange.SetBitmap(self.load_icon('color_change'))
self.btnColorChange.Bind(wx.EVT_TOGGLEBUTTON, lambda event: self.on_marker_button('color_change', event))
if self.detach_callback:
self.btnDetachSimulator = wx.BitmapButton(self, -1, style=self.button_style)
self.btnDetachSimulator.SetToolTip(_('Detach/attach simulator window'))
self.btnDetachSimulator.SetBitmap(self.load_icon('detach_window'))
self.btnDetachSimulator.Bind(wx.EVT_BUTTON, lambda event: self.detach_callback())
# Layout
self.hbSizer1 = wx.BoxSizer(wx.HORIZONTAL)
self.hbSizer1.Add(self.slider, 1, wx.EXPAND | wx.RIGHT, 10)
self.hbSizer1.Add(self.stitchBox, 0, wx.ALIGN_CENTER | wx.RIGHT, 10)
self.hbSizer1.Add(self.totalstitchText, 0, wx.ALIGN_CENTER | wx.RIGHT, 10)
self.hbSizer1.Add(self.stitchBox, 0, wx.ALIGN_CENTER | wx.Right, 10)
self.hbSizer1.Add(self.totalstitchText, 0, wx.ALIGN_CENTER | wx.LEFT, 10)
self.controls_sizer = wx.StaticBoxSizer(wx.StaticBox(self, wx.ID_ANY, _("Controls")), wx.HORIZONTAL)
self.controls_inner_sizer = wx.BoxSizer(wx.HORIZONTAL)
@ -151,6 +158,8 @@ class ControlPanel(wx.Panel):
self.show_inner_sizer.Add(self.btnTrim, 0, wx.ALL, 2)
self.show_inner_sizer.Add(self.btnStop, 0, wx.ALL, 2)
self.show_inner_sizer.Add(self.btnColorChange, 0, wx.ALL, 2)
if self.detach_callback:
self.show_inner_sizer.Add(self.btnDetachSimulator, 0, wx.ALL, 2)
self.show_sizer.Add((1, 1), 1)
self.show_sizer.Add(self.show_inner_sizer, 0, wx.ALIGN_CENTER_VERTICAL | wx.ALL, 10)
self.show_sizer.Add((1, 1), 1)
@ -209,7 +218,6 @@ class ControlPanel(wx.Panel):
(wx.ACCEL_NORMAL, ord('o'), self.on_toggle_npp_shortcut),
(wx.ACCEL_NORMAL, ord('p'), self.play_or_pause),
(wx.ACCEL_NORMAL, wx.WXK_SPACE, self.play_or_pause),
(wx.ACCEL_NORMAL, ord('q'), self.animation_quit),
(wx.ACCEL_NORMAL, wx.WXK_PAGEDOWN, self.animation_one_command_backward),
(wx.ACCEL_NORMAL, wx.WXK_PAGEUP, self.animation_one_command_forward),
@ -227,7 +235,8 @@ class ControlPanel(wx.Panel):
self.SetFocus()
# wait for layouts so that panel size is set
wx.CallLater(50, self.load, self.stitch_plan)
if self.stitch_plan:
wx.CallLater(50, self.load, self.stitch_plan)
def set_drawing_panel(self, drawing_panel):
self.drawing_panel = drawing_panel
@ -240,6 +249,7 @@ class ControlPanel(wx.Panel):
self.num_stitches = num_stitches
self.stitchBox.SetMax(num_stitches)
self.slider.SetMax(num_stitches)
self.totalstitchText.SetLabel(f"/ { num_stitches }")
self.choose_speed()
def add_color(self, color, num_stitches):
@ -423,9 +433,6 @@ class ControlPanel(wx.Panel):
stitch_number += 1
self.drawing_panel.set_current_stitch(stitch_number)
def animation_quit(self, event):
self.parent.quit()
def animation_restart(self, event):
self.drawing_panel.restart()
@ -454,7 +461,7 @@ class DrawingPanel(wx.Panel):
def __init__(self, *args, **kwargs):
""""""
self.stitch_plan = kwargs.pop('stitch_plan')
self.stitch_plan = kwargs.pop('stitch_plan', None)
self.control_panel = kwargs.pop('control_panel')
kwargs['style'] = wx.BORDER_SUNKEN
wx.Panel.__init__(self, *args, **kwargs)
@ -483,8 +490,11 @@ class DrawingPanel(wx.Panel):
self.Bind(wx.EVT_LEFT_DOWN, self.on_left_mouse_button_down)
self.Bind(wx.EVT_MOUSEWHEEL, self.on_mouse_wheel)
self.SetMinSize((400, 400))
# wait for layouts so that panel size is set
wx.CallLater(50, self.load, self.stitch_plan)
if self.stitch_plan:
wx.CallLater(50, self.load, self.stitch_plan)
def clamp_current_stitch(self):
if self.current_stitch < 1:
@ -520,10 +530,12 @@ class DrawingPanel(wx.Panel):
wx.CallLater(int(1000 * frame_time), self.animate)
def OnPaint(self, e):
dc = wx.PaintDC(self)
if not self.loaded:
dc.Clear()
return
dc = wx.PaintDC(self)
canvas = wx.GraphicsContext.Create(dc)
self.draw_stitches(canvas)
@ -616,8 +628,8 @@ class DrawingPanel(wx.Panel):
canvas.StrokeLineSegments(stitches, [(stitch[0] + 0.001, stitch[1]) for stitch in stitches])
def clear(self):
dc = wx.ClientDC(self)
dc.Clear()
self.loaded = False
self.Refresh()
def load(self, stitch_plan):
self.current_stitch = 1
@ -722,7 +734,7 @@ class DrawingPanel(wx.Panel):
command = self.commands[self.current_stitch]
self.control_panel.on_current_stitch(self.current_stitch, command)
statusbar = self.GetTopLevelParent().statusbar
statusbar.SetStatusText(_("Command: %s") % COMMAND_NAMES[command])
statusbar.SetStatusText(_("Command: %s") % COMMAND_NAMES[command], 1)
self.stop_if_at_end()
self.Refresh()
@ -1012,19 +1024,15 @@ class SimulatorSlider(wx.Panel):
class SimulatorPanel(wx.Panel):
""""""
def __init__(self, parent, *args, **kwargs):
def __init__(self, parent, stitch_plan=None, target_duration=5, stitches_per_second=16, detach_callback=None):
""""""
self.parent = parent
stitch_plan = kwargs.pop('stitch_plan')
target_duration = kwargs.pop('target_duration')
stitches_per_second = kwargs.pop('stitches_per_second')
kwargs['style'] = wx.BORDER_SUNKEN
wx.Panel.__init__(self, parent, *args, **kwargs)
super().__init__(parent, style=wx.BORDER_SUNKEN)
self.cp = ControlPanel(self,
stitch_plan=stitch_plan,
stitches_per_second=stitches_per_second,
target_duration=target_duration)
target_duration=target_duration,
detach_callback=detach_callback)
self.dp = DrawingPanel(self, stitch_plan=stitch_plan, control_panel=self.cp)
self.cp.set_drawing_panel(self.dp)
@ -1033,9 +1041,6 @@ class SimulatorPanel(wx.Panel):
vbSizer.Add(self.cp, 0, wx.EXPAND | wx.ALL, 2)
self.SetSizerAndFit(vbSizer)
def quit(self):
self.parent.quit()
def go(self):
self.dp.go()
@ -1050,108 +1055,161 @@ class SimulatorPanel(wx.Panel):
self.dp.clear()
class EmbroiderySimulator(wx.Frame):
def __init__(self, *args, **kwargs):
self.on_close_hook = kwargs.pop('on_close', None)
stitch_plan = kwargs.pop('stitch_plan', None)
stitches_per_second = kwargs.pop('stitches_per_second', 16)
target_duration = kwargs.pop('target_duration', None)
wx.Frame.__init__(self, *args, **kwargs)
class SimulatorWindow(wx.Frame):
def __init__(self, panel=None, parent=None, **kwargs):
super().__init__(None, title=_("Embroidery Simulation"), **kwargs)
self.SetWindowStyle(wx.FRAME_FLOAT_ON_PARENT)
self.SetWindowStyle(wx.FRAME_FLOAT_ON_PARENT | wx.DEFAULT_FRAME_STYLE)
sizer = wx.BoxSizer(wx.HORIZONTAL)
self.simulator_panel = SimulatorPanel(self,
stitch_plan=stitch_plan,
target_duration=target_duration,
stitches_per_second=stitches_per_second)
sizer.Add(self.simulator_panel, 1, wx.EXPAND)
self.sizer = wx.BoxSizer(wx.VERTICAL)
self.statusbar = self.CreateStatusBar()
self.statusbar = self.CreateStatusBar(2)
self.statusbar.SetStatusWidths((0, -1))
# SetSizeHints seems to be ignored in macOS, so we have to adjust size manually
# self.SetSizeHints(sizer.CalcMin())
frame_width, frame_height = self.GetSize()
sizer_width, sizer_height = sizer.CalcMin()
size_diff = frame_width - sizer_width
if size_diff < 0:
frame_x, frame_y = self.GetPosition()
self.SetPosition((frame_x + size_diff, frame_y))
self.SetSize((sizer_width, frame_height))
if panel and parent:
self.is_child = True
self.panel = panel
self.parent = parent
self.panel.Reparent(self)
self.sizer.Add(self.panel, 1, wx.EXPAND)
self.panel.Show()
else:
self.is_child = False
self.simulator_panel = SimulatorPanel(self)
self.sizer.Add(self.simulator_panel, 1, wx.EXPAND)
self.Bind(wx.EVT_CLOSE, self.on_close)
self.SetSizer(self.sizer)
self.Layout()
def quit(self):
self.Close()
self.SetMinSize(self.sizer.CalcMin())
if self.is_child:
self.Bind(wx.EVT_CLOSE, self.on_close)
def detach_simulator_panel(self):
self.sizer.Detach(self.panel)
def on_close(self, event):
self.simulator_panel.stop()
self.parent.attach_simulator()
if self.on_close_hook:
self.on_close_hook()
self.SetFocus()
class SplitSimulatorWindow(wx.Frame):
def __init__(self, panel_class, title, target_duration=None, **kwargs):
super().__init__(None, title=title)
self.SetWindowStyle(wx.FRAME_FLOAT_ON_PARENT | wx.DEFAULT_FRAME_STYLE)
self.detached_simulator_frame = None
self.splitter = wx.SplitterWindow(self, style=wx.SP_LIVE_UPDATE)
self.simulator_panel = SimulatorPanel(self.splitter, target_duration=target_duration, detach_callback=self.toggle_detach_simulator)
self.settings_panel = panel_class(self.splitter, simulator=self.simulator_panel, **kwargs)
self.splitter.SplitVertically(self.settings_panel, self.simulator_panel)
self.splitter.SetMinimumPaneSize(100)
icon = wx.Icon(os.path.join(get_resource_dir("icons"), "inkstitch256x256.png"))
self.SetIcon(icon)
self.statusbar = self.CreateStatusBar(2)
self.sizer = wx.BoxSizer(wx.VERTICAL)
self.sizer.Add(self.splitter, 1, wx.EXPAND)
self.SetSizer(self.sizer)
self.SetMinSize(self.sizer.CalcMin())
self.Maximize()
self.Show()
wx.CallLater(100, self.set_sash_position)
self.Bind(wx.EVT_SPLITTER_SASH_POS_CHANGING, self.splitter_resize)
self.Bind(wx.EVT_CLOSE, self.on_close)
if global_settings['pop_out_simulator']:
self.detach_simulator()
def splitter_resize(self, event):
self.statusbar.SetStatusWidths((self.simulator_panel.GetScreenPosition()[0], -1))
def set_sash_position(self):
settings_panel_min_size = self.settings_panel.GetSizer().CalcMin()
debug.log(f"{settings_panel_min_size=}")
self.splitter.SetSashPosition(settings_panel_min_size.width)
self.statusbar.SetStatusWidths((settings_panel_min_size.width, -1))
def on_close(self, event):
if self.detached_simulator_frame:
self.detached_simulator_frame.Destroy()
self.Destroy()
def go(self):
self.simulator_panel.go()
def toggle_detach_simulator(self):
if self.detached_simulator_frame:
self.attach_simulator()
else:
self.detach_simulator()
def stop(self):
self.simulator_panel.stop()
def attach_simulator(self):
self.detached_simulator_frame.detach_simulator_panel()
self.simulator_panel.Reparent(self.splitter)
self.splitter.SplitVertically(self.settings_panel, self.simulator_panel)
def load(self, stitch_plan):
self.simulator_panel.load(stitch_plan)
self.GetStatusBar().SetStatusText(self.detached_simulator_frame.GetStatusBar().GetStatusText(1), 1)
def clear(self):
self.simulator_panel.clear()
self.detached_simulator_frame.Destroy()
self.detached_simulator_frame = None
self.Maximize()
self.splitter.UpdateSize()
self.SetFocus()
self.Raise()
wx.CallLater(100, self.set_sash_position)
global_settings['pop_out_simulator'] = False
def detach_simulator(self):
self.splitter.Unsplit()
self.detached_simulator_frame = SimulatorWindow(panel=self.simulator_panel, parent=self)
self.splitter.SetMinimumPaneSize(100)
current_screen = wx.Display.GetFromPoint(wx.GetMousePosition())
display = wx.Display(current_screen)
screen_rect = display.GetClientArea()
settings_panel_size = self.settings_panel.GetSizer().CalcMin()
self.SetMinSize(settings_panel_size)
self.Maximize(False)
self.SetSize((settings_panel_size.width, screen_rect.height))
self.SetPosition((screen_rect.left, screen_rect.top))
self.detached_simulator_frame.SetSize((screen_rect.width - settings_panel_size.width, screen_rect.height))
self.detached_simulator_frame.SetPosition((settings_panel_size.width, screen_rect.top))
self.detached_simulator_frame.GetStatusBar().SetStatusText(self.GetStatusBar().GetStatusText(1), 1)
self.GetStatusBar().SetStatusText("", 1)
self.detached_simulator_frame.Show()
global_settings['pop_out_simulator'] = True
class SimulatorPreview(Thread):
"""Manages a preview simulation and a background thread for generating patches."""
class PreviewRenderer(Thread):
"""Render stitch plan in a background thread."""
def __init__(self, parent, *args, **kwargs):
"""Construct a SimulatorPreview.
The parent is expected to be a wx.Window and also implement the following methods:
def generate_patches(self, abort_event):
Produce an list of StitchGroup instances. This method will be
invoked in a background thread and it is expected that it may
take awhile.
If possible, this method should periodically check
abort_event.is_set(), and if True, stop early. The return
value will be ignored in this case.
"""
self.parent = parent
self.target_duration = kwargs.pop('target_duration', 5)
super(SimulatorPreview, self).__init__(*args, **kwargs)
def __init__(self, render_stitch_plan_hook, rendering_completed_hook):
super(PreviewRenderer, self).__init__()
self.daemon = True
self.simulate_window = None
self.refresh_needed = Event()
self.render_stitch_plan_hook = render_stitch_plan_hook
self.rendering_completed_hook = rendering_completed_hook
# This is read by utils.threading.check_stop_flag() to abort stitch plan
# generation.
self.stop = Event()
# used when closing to avoid having the window reopen at the last second
self._disabled = False
wx.CallLater(1000, self.update)
def disable(self):
self._disabled = True
def update(self):
"""Request an update of the simulator preview with freshly-generated patches."""
"""Request to render a new stitch plan.
if self.simulate_window:
self.simulate_window.stop()
self.simulate_window.clear()
if self._disabled:
return
self.render_stitch_plan_hook() will be called in a background thread, and then
self.rendering_completed_hook() will be called with the resulting stitch plan.
"""
if not self.is_alive():
self.start()
@ -1167,80 +1225,22 @@ class SimulatorPreview(Thread):
try:
debug.log("update_patches")
self.update_patches()
self.render_stitch_plan()
except ExitThread:
debug.log("ExitThread caught")
self.stop.clear()
def update_patches(self):
def render_stitch_plan(self):
try:
patches = self.parent.generate_patches(self.refresh_needed)
stitch_plan = self.render_stitch_plan_hook()
if stitch_plan:
# rendering_completed() will be called in the main thread.
wx.CallAfter(self.rendering_completed_hook, stitch_plan)
except ExitThread:
raise
except: # noqa: E722
# If something goes wrong when rendering patches, it's not great,
# but we don't really want the simulator thread to crash. Instead,
# just swallow the exception and abort. It'll show up when they
# try to actually embroider the shape.
return
if patches and not self.refresh_needed.is_set():
metadata = self.parent.metadata
collapse_len = metadata['collapse_len_mm']
min_stitch_len = metadata['min_stitch_len_mm']
stitch_plan = stitch_groups_to_stitch_plan(patches, collapse_len=collapse_len, min_stitch_len=min_stitch_len)
# GUI stuff needs to happen in the main thread, so we ask the main
# thread to call refresh_simulator().
wx.CallAfter(self.refresh_simulator, patches, stitch_plan)
def refresh_simulator(self, patches, stitch_plan):
if self.simulate_window:
self.simulate_window.stop()
self.simulate_window.load(stitch_plan)
else:
params_rect = self.parent.GetScreenRect()
simulator_pos = params_rect.GetTopRight()
simulator_pos.x += 5
current_screen = wx.Display.GetFromPoint(wx.GetMousePosition())
display = wx.Display(current_screen)
screen_rect = display.GetClientArea()
simulator_pos.y = screen_rect.GetTop()
width = screen_rect.GetWidth() - params_rect.GetWidth()
height = screen_rect.GetHeight()
try:
self.simulate_window = EmbroiderySimulator(None, -1, _("Preview"),
simulator_pos,
size=(width, height),
stitch_plan=stitch_plan,
on_close=self.simulate_window_closed,
target_duration=self.target_duration)
except Exception:
import traceback
print(traceback.format_exc(), file=sys.stderr)
try:
# a window may have been created, so we need to destroy it
# or the app will never exit
wx.Window.FindWindowByName(_("Preview")).Destroy()
except Exception:
pass
self.simulate_window.Show()
wx.CallLater(10, self.parent.Raise)
wx.CallAfter(self.simulate_window.go)
def simulate_window_closed(self):
self.simulate_window = None
def close(self):
self.disable()
if self.simulate_window:
self.simulate_window.stop()
self.simulate_window.Close()
import traceback
debug.log("unhandled exception in PreviewRenderer.render_stitch_plan(): " + traceback.format_exc())
def show_simulator(stitch_plan):
@ -1255,7 +1255,7 @@ def show_simulator(stitch_plan):
width = screen_rect[2] - 1
height = screen_rect[3] - 1
frame = EmbroiderySimulator(None, -1, _("Embroidery Simulation"), pos=simulator_pos, size=(width, height), stitch_plan=stitch_plan)
frame = SimulatorWindow(pos=simulator_pos, size=(width, height), stitch_plan=stitch_plan)
app.SetTopWindow(frame)
frame.Show()
app.MainLoop()

Wyświetl plik

@ -20,7 +20,7 @@ class GenerateSwatchesFrame(wx.Frame):
wx.Frame.__init__(self, *args, **kwargs)
wx.Frame.__init__(self, None, wx.ID_ANY, _("Generate Swatches"), *args, **kwargs)
self.SetWindowStyle(wx.FRAME_FLOAT_ON_PARENT)
self.SetWindowStyle(wx.FRAME_FLOAT_ON_PARENT | wx.DEFAULT_FRAME_STYLE)
self.panel = wx.Panel(self, wx.ID_ANY)

Wyświetl plik

@ -12,7 +12,8 @@ DEFAULT_METADATA = {
}
DEFAULT_SETTINGS = {
"cache_size": 100
"cache_size": 100,
"pop_out_simulator": False
}

Wyświetl plik

@ -17,6 +17,7 @@ _default_stop_flag = threading.Event()
def check_stop_flag():
# This getattr() actually looks at the PreviewRenderer instance's stop attribute.
if getattr(threading.current_thread(), 'stop', _default_stop_flag).is_set():
debug.log("exiting thread")
raise ExitThread()