summaryrefslogtreecommitdiff
path: root/lib/gui/simulator.py
diff options
context:
space:
mode:
authorKaalleen <36401965+kaalleen@users.noreply.github.com>2024-06-14 09:49:57 +0200
committerGitHub <noreply@github.com>2024-06-14 09:49:57 +0200
commitdbdba2cda3a66fc42b5bad05e88d33b845a85e2f (patch)
tree6d50270e5d687a939ec75c661b1603bc43db1f4e /lib/gui/simulator.py
parent39d9defef4a6c813e40df9a8de254af422af6ccd (diff)
Add preferences button to simulator (#2992)
* split simulator panel files * add view panel to position view options at the side * fix single simulator start size (macOS)
Diffstat (limited to 'lib/gui/simulator.py')
-rw-r--r--lib/gui/simulator.py1328
1 files changed, 0 insertions, 1328 deletions
diff --git a/lib/gui/simulator.py b/lib/gui/simulator.py
deleted file mode 100644
index 006d0e57..00000000
--- a/lib/gui/simulator.py
+++ /dev/null
@@ -1,1328 +0,0 @@
-# Authors: see git history
-#
-# Copyright (c) 2010 Authors
-# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
-import os
-import sys
-import time
-from threading import Event, Thread
-
-import wx
-from numpy import split
-from wx.lib.intctrl import IntCtrl
-
-from lib.debug.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 ..svg import PIXELS_PER_MM
-
-# L10N command label at bottom of simulator window
-COMMAND_NAMES = [_("STITCH"), _("JUMP"), _("TRIM"), _("STOP"), _("COLOR CHANGE")]
-
-STITCH = 0
-JUMP = 1
-TRIM = 2
-STOP = 3
-COLOR_CHANGE = 4
-
-
-class ControlPanel(wx.Panel):
- """"""
-
- @debug.time
- def __init__(self, parent, *args, **kwargs):
- """"""
- self.parent = parent
- 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
- wx.Panel.__init__(self, parent, *args, **kwargs)
-
- self.drawing_panel = None
- self.num_stitches = 0
- self.current_stitch = 0
- self.speed = 1
- self.direction = 1
- self._last_color_block_end = 0
-
- self.icons_dir = get_resource_dir("icons")
-
- # Widgets
- self.button_size = self.GetTextExtent("M").y * 2
- self.button_style = wx.BU_EXACTFIT | wx.BU_NOTEXT
- self.btnMinus = wx.Button(self, -1, style=self.button_style)
- self.btnMinus.Bind(wx.EVT_BUTTON, self.animation_slow_down)
- self.btnMinus.SetBitmap(self.load_icon('slower'))
- self.btnMinus.SetToolTip(_('Slow down (arrow down)'))
- self.btnPlus = wx.Button(self, -1, style=self.button_style)
- self.btnPlus.Bind(wx.EVT_BUTTON, self.animation_speed_up)
- self.btnPlus.SetBitmap(self.load_icon('faster'))
- self.btnPlus.SetToolTip(_('Speed up (arrow up)'))
- self.btnBackwardStitch = wx.Button(self, -1, style=self.button_style)
- self.btnBackwardStitch.Bind(wx.EVT_BUTTON, self.animation_one_stitch_backward)
- self.btnBackwardStitch.SetBitmap(self.load_icon('backward_stitch'))
- self.btnBackwardStitch.SetToolTip(_('Go backward one stitch (-)'))
- self.btnForwardStitch = wx.Button(self, -1, style=self.button_style)
- self.btnForwardStitch.Bind(wx.EVT_BUTTON, self.animation_one_stitch_forward)
- self.btnForwardStitch.SetBitmap(self.load_icon('forward_stitch'))
- self.btnForwardStitch.SetToolTip(_('Go forward one stitch (+)'))
- self.btnBackwardCommand = wx.Button(self, -1, style=self.button_style)
- self.btnBackwardCommand.Bind(wx.EVT_BUTTON, self.animation_one_command_backward)
- self.btnBackwardCommand.SetBitmap(self.load_icon('backward_command'))
- self.btnBackwardCommand.SetToolTip(_('Go backward one command (page-down)'))
- self.btnForwardCommand = wx.Button(self, -1, style=self.button_style)
- self.btnForwardCommand.Bind(wx.EVT_BUTTON, self.animation_one_command_forward)
- self.btnForwardCommand.SetBitmap(self.load_icon('forward_command'))
- self.btnForwardCommand.SetToolTip(_('Go forward one command (page-up)'))
- self.btnDirection = wx.Button(self, -1, style=self.button_style)
- self.btnDirection.Bind(wx.EVT_BUTTON, self.on_direction_button)
- self.btnDirection.SetBitmap(self.load_icon('direction'))
- self.btnDirection.SetToolTip(_('Switch animation direction (arrow left, arrow right)'))
- self.btnPlay = wx.BitmapToggleButton(self, -1, style=self.button_style)
- self.btnPlay.Bind(wx.EVT_TOGGLEBUTTON, self.on_play_button)
- self.btnPlay.SetBitmap(self.load_icon('play'))
- self.btnPlay.SetToolTip(_('Play (P)'))
- self.btnRestart = wx.Button(self, -1, style=self.button_style)
- self.btnRestart.Bind(wx.EVT_BUTTON, self.animation_restart)
- self.btnRestart.SetBitmap(self.load_icon('restart'))
- self.btnRestart.SetToolTip(_('Restart (R)'))
- self.btnNpp = wx.BitmapToggleButton(self, -1, style=self.button_style)
- 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=2)
- self.slider.Bind(wx.EVT_SLIDER, self.on_slider)
- 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.Clear()
- 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="")
- extent = self.totalstitchText.GetTextExtent("0000000")
- self.totalstitchText.SetMinSize(extent)
- self.btnJump = wx.BitmapToggleButton(self, -1, style=self.button_style)
- self.btnJump.SetToolTip(_('Show jump stitches'))
- self.btnJump.SetBitmap(self.load_icon('jump'))
- self.btnJump.Bind(wx.EVT_TOGGLEBUTTON, lambda event: self.on_marker_button('jump', event))
- self.btnTrim = wx.BitmapToggleButton(self, -1, style=self.button_style)
- self.btnTrim.SetToolTip(_('Show trims'))
- self.btnTrim.SetBitmap(self.load_icon('trim'))
- self.btnTrim.Bind(wx.EVT_TOGGLEBUTTON, lambda event: self.on_marker_button('trim', event))
- self.btnStop = wx.BitmapToggleButton(self, -1, style=self.button_style)
- self.btnStop.SetToolTip(_('Show stops'))
- self.btnStop.SetBitmap(self.load_icon('stop'))
- self.btnStop.Bind(wx.EVT_TOGGLEBUTTON, lambda event: self.on_marker_button('stop', event))
- self.btnColorChange = wx.BitmapToggleButton(self, -1, style=self.button_style)
- 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))
- self.btnBackgroundColor = wx.ColourPickerCtrl(self, -1, colour='white', size=((40, -1)))
- self.btnBackgroundColor.SetToolTip(_("Change background color"))
- self.btnBackgroundColor.Bind(wx.EVT_COLOURPICKER_CHANGED, self.on_update_background_color)
- 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_TOP | wx.TOP, 25)
- self.hbSizer1.Add((1, 1), 0, wx.RIGHT, 10)
- self.hbSizer1.Add(self.totalstitchText, 0, wx.ALIGN_TOP | wx.TOP, 25)
- self.hbSizer1.Add((1, 1), 0, wx.RIGHT, 10)
-
- self.controls_sizer = wx.StaticBoxSizer(wx.StaticBox(self, wx.ID_ANY, _("Controls")), wx.HORIZONTAL)
- self.controls_inner_sizer = wx.BoxSizer(wx.HORIZONTAL)
- self.controls_inner_sizer.Add(self.btnBackwardCommand, 0, wx.EXPAND | wx.ALL, 2)
- self.controls_inner_sizer.Add(self.btnBackwardStitch, 0, wx.EXPAND | wx.ALL, 2)
- self.controls_inner_sizer.Add(self.btnForwardStitch, 0, wx.EXPAND | wx.ALL, 2)
- self.controls_inner_sizer.Add(self.btnForwardCommand, 0, wx.EXPAND | wx.ALL, 2)
- self.controls_inner_sizer.Add(self.btnDirection, 0, wx.EXPAND | wx.ALL, 2)
- self.controls_inner_sizer.Add(self.btnPlay, 0, wx.EXPAND | wx.ALL, 2)
- self.controls_inner_sizer.Add(self.btnRestart, 0, wx.EXPAND | wx.ALL, 2)
- self.controls_sizer.Add((1, 1), 1)
- self.controls_sizer.Add(self.controls_inner_sizer, 0, wx.ALIGN_CENTER_VERTICAL | wx.ALL, 10)
- self.controls_sizer.Add((1, 1), 1)
-
- self.show_sizer = wx.StaticBoxSizer(wx.StaticBox(self, wx.ID_ANY, _("Show")), wx.HORIZONTAL)
- self.show_inner_sizer = wx.BoxSizer(wx.HORIZONTAL)
- self.show_inner_sizer.Add(self.btnNpp, 0, wx.ALL, 2)
- self.show_inner_sizer.Add(self.btnJump, 0, wx.ALL, 2)
- 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)
- self.show_inner_sizer.Add(self.btnBackgroundColor, 0, wx.EXPAND | 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)
-
- self.speed_sizer = wx.StaticBoxSizer(wx.StaticBox(self, wx.ID_ANY, _("Speed")), wx.VERTICAL)
-
- self.speed_buttons_sizer = wx.BoxSizer(wx.HORIZONTAL)
- self.speed_buttons_sizer.Add((1, 1), 1)
- self.speed_buttons_sizer.Add(self.btnMinus, 0, wx.ALL, 2)
- self.speed_buttons_sizer.Add(self.btnPlus, 0, wx.ALL, 2)
- self.speed_buttons_sizer.Add((1, 1), 1)
- self.speed_sizer.Add(self.speed_buttons_sizer, 0, wx.EXPAND | wx.ALL)
- self.speed_text = wx.StaticText(self, wx.ID_ANY, label="", style=wx.ALIGN_CENTRE_HORIZONTAL | wx.ST_NO_AUTORESIZE)
- self.speed_text.SetFont(wx.Font(wx.FontInfo(10).Bold()))
- extent = self.speed_text.GetTextExtent(self.format_speed_text(100000))
- self.speed_text.SetMinSize(extent)
- self.speed_sizer.Add(self.speed_text, 0, wx.EXPAND | wx.ALL, 5)
-
- # A normal BoxSizer can only make child components the same or
- # proportional size. A FlexGridSizer can split up the available extra
- # space evenly among all growable columns.
- self.control_row2_sizer = wx.FlexGridSizer(cols=3, vgap=0, hgap=5)
- self.control_row2_sizer.AddGrowableCol(0)
- self.control_row2_sizer.AddGrowableCol(1)
- self.control_row2_sizer.AddGrowableCol(2)
- self.control_row2_sizer.Add(self.controls_sizer, 0, wx.EXPAND)
- self.control_row2_sizer.Add(self.speed_sizer, 0, wx.EXPAND)
- self.control_row2_sizer.Add(self.show_sizer, 0, wx.EXPAND)
-
- self.vbSizer = vbSizer = wx.BoxSizer(wx.VERTICAL)
- vbSizer.Add(self.hbSizer1, 1, wx.EXPAND | wx.ALL, 10)
- vbSizer.Add(self.control_row2_sizer, 0, wx.EXPAND | wx.LEFT | wx.RIGHT | wx.BOTTOM, 10)
- self.SetSizerAndFit(vbSizer)
-
- # Keyboard Shortcuts
- shortcut_keys = [
- (wx.ACCEL_NORMAL, wx.WXK_RIGHT, self.animation_forward),
- (wx.ACCEL_NORMAL, wx.WXK_NUMPAD_RIGHT, self.animation_forward),
- (wx.ACCEL_NORMAL, wx.WXK_LEFT, self.animation_reverse),
- (wx.ACCEL_NORMAL, wx.WXK_NUMPAD_LEFT, self.animation_reverse),
- (wx.ACCEL_NORMAL, wx.WXK_UP, self.animation_speed_up),
- (wx.ACCEL_NORMAL, wx.WXK_NUMPAD_UP, self.animation_speed_up),
- (wx.ACCEL_NORMAL, wx.WXK_DOWN, self.animation_slow_down),
- (wx.ACCEL_NORMAL, wx.WXK_NUMPAD_DOWN, self.animation_slow_down),
- (wx.ACCEL_NORMAL, ord('+'), self.animation_one_stitch_forward),
- (wx.ACCEL_NORMAL, ord('='), self.animation_one_stitch_forward),
- (wx.ACCEL_SHIFT, ord('='), self.animation_one_stitch_forward),
- (wx.ACCEL_NORMAL, wx.WXK_ADD, self.animation_one_stitch_forward),
- (wx.ACCEL_NORMAL, wx.WXK_NUMPAD_ADD, self.animation_one_stitch_forward),
- (wx.ACCEL_NORMAL, wx.WXK_NUMPAD_UP, self.animation_one_stitch_forward),
- (wx.ACCEL_NORMAL, ord('-'), self.animation_one_stitch_backward),
- (wx.ACCEL_NORMAL, ord('_'), self.animation_one_stitch_backward),
- (wx.ACCEL_NORMAL, wx.WXK_SUBTRACT, self.animation_one_stitch_backward),
- (wx.ACCEL_NORMAL, wx.WXK_NUMPAD_SUBTRACT, self.animation_one_stitch_backward),
- (wx.ACCEL_NORMAL, ord('r'), self.animation_restart),
- (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, wx.WXK_PAGEDOWN, self.animation_one_command_backward),
- (wx.ACCEL_NORMAL, wx.WXK_PAGEUP, self.animation_one_command_forward),
-
- ]
-
- self.accel_entries = []
-
- for shortcut_key in shortcut_keys:
- eventId = wx.NewIdRef()
- self.accel_entries.append((shortcut_key[0], shortcut_key[1], eventId))
- self.Bind(wx.EVT_MENU, shortcut_key[2], id=eventId)
-
- self.accel_table = wx.AcceleratorTable(self.accel_entries)
- self.SetAcceleratorTable(self.accel_table)
-
- # wait for layouts so that panel size is set
- if self.stitch_plan:
- wx.CallLater(50, self.load, self.stitch_plan)
-
- def set_drawing_panel(self, drawing_panel):
- self.drawing_panel = drawing_panel
- self.drawing_panel.set_speed(self.speed)
-
- def _set_num_stitches(self, num_stitches):
- if num_stitches < 2:
- # otherwise the slider and intctrl get mad
- num_stitches = 2
- self.num_stitches = num_stitches
- self.stitchBox.SetValue(1)
- self.stitchBox.SetMax(num_stitches)
- self.slider.SetMax(num_stitches)
- self.totalstitchText.SetLabel(f"/ { num_stitches }")
- self.choose_speed()
-
- def clear(self):
- self.stitches = []
- self._set_num_stitches(0)
- self.slider.clear()
- self.stitchBox.Clear()
- self.totalstitchText.SetLabel("")
-
- def load(self, stitch_plan):
- self.clear()
- self.stitches = []
- self._set_num_stitches(stitch_plan.num_stitches)
-
- stitch_num = 0
- last_block_end = 1
- for color_block in stitch_plan.color_blocks:
- self.stitches.extend(color_block.stitches)
-
- start = stitch_num + 1
- end = start + color_block.num_stitches - 1
- self.slider.add_color_section(color_block.color.rgb, last_block_end, end)
- last_block_end = end
-
- for stitch_num, stitch in enumerate(color_block.stitches, start):
- if stitch.trim:
- self.slider.add_marker("trim", stitch_num)
- elif stitch.stop:
- self.slider.add_marker("stop", stitch_num)
- elif stitch.jump:
- self.slider.add_marker("jump", stitch_num)
- elif stitch.color_change:
- self.slider.add_marker("color_change", stitch_num)
-
- def is_dark_theme(self):
- return wx.SystemSettings().GetAppearance().IsDark()
-
- def load_icon(self, icon_name):
- if self.is_dark_theme():
- icon = wx.Image(os.path.join(self.icons_dir, f"{icon_name}_dark.png"))
- else:
- icon = wx.Image(os.path.join(self.icons_dir, f"{icon_name}.png"))
- icon.Rescale(self.button_size, self.button_size, wx.IMAGE_QUALITY_HIGH)
- return icon.ConvertToBitmap()
-
- def on_marker_button(self, marker_type, event):
- self.slider.enable_marker_list(marker_type, event.GetEventObject().GetValue())
- if marker_type == 'jump':
- self.drawing_panel.Refresh()
-
- def on_update_background_color(self, event):
- self.set_background_color(event.Colour)
-
- def set_background_color(self, color):
- self.btnBackgroundColor.SetColour(color)
- self.drawing_panel.SetBackgroundColour(color)
- self.drawing_panel.Refresh()
-
- def choose_speed(self):
- if self.target_duration:
- self.set_speed(int(self.num_stitches / float(self.target_duration)))
- else:
- self.set_speed(self.target_stitches_per_second)
-
- def animation_forward(self, event=None):
- self.drawing_panel.forward()
- self.direction = 1
- self.update_speed_text()
-
- def animation_reverse(self, event=None):
- self.drawing_panel.reverse()
- self.direction = -1
- self.update_speed_text()
-
- def on_direction_button(self, event):
- if self.direction == -1:
- self.animation_forward()
- else:
- self.animation_reverse()
-
- def set_speed(self, speed):
- self.speed = int(max(speed, 1))
- self.update_speed_text()
-
- if self.drawing_panel:
- self.drawing_panel.set_speed(self.speed)
-
- def format_speed_text(self, speed):
- return _('%d stitches/sec') % speed
-
- def update_speed_text(self):
- self.speed_text.SetLabel(self.format_speed_text(self.speed * self.direction))
-
- def on_slider(self, event):
- self.animation_pause()
- stitch = event.GetEventObject().GetValue()
- self.stitchBox.SetValue(stitch)
-
- if self.drawing_panel:
- self.drawing_panel.set_current_stitch(stitch)
-
- self.parent.SetFocus()
-
- def on_current_stitch(self, stitch, command):
- if self.current_stitch != stitch:
- self.current_stitch = stitch
- self.slider.SetValue(stitch)
- self.stitchBox.SetValue(stitch)
-
- def on_stitch_box_focus(self, event):
- self.animation_pause()
- self.SetAcceleratorTable(wx.AcceleratorTable([]))
- event.Skip()
-
- def on_stitch_box_focusout(self, event):
- self.SetAcceleratorTable(self.accel_table)
- stitch = self.stitchBox.GetValue()
- # We now want to remove the focus from the stitchBox.
- # In Windows it won't work if we set focus to self.parent, while setting the focus to the
- # top level would work. This in turn would activate the trim button in Linux. So let's
- # set the focus on the slider instead where it doesn't cause any harm in any of the operating systems
- self.slider.SetFocus()
-
- if stitch is None:
- stitch = 1
- self.stitchBox.SetValue(1)
-
- self.slider.SetValue(stitch)
-
- if self.drawing_panel:
- self.drawing_panel.set_current_stitch(stitch)
- event.Skip()
-
- def animation_slow_down(self, event):
- """"""
- self.set_speed(self.speed / 2.0)
-
- def animation_speed_up(self, event):
- """"""
- self.set_speed(self.speed * 2.0)
-
- def animation_pause(self, event=None):
- self.drawing_panel.stop()
-
- def animation_start(self, event=None):
- self.drawing_panel.go()
-
- def on_start(self):
- self.btnPlay.SetValue(True)
-
- def on_stop(self):
- self.btnPlay.SetValue(False)
-
- def on_play_button(self, event):
- play = self.btnPlay.GetValue()
- if play:
- self.animation_start()
- else:
- self.animation_pause()
-
- def play_or_pause(self, event):
- if self.drawing_panel.animating:
- self.animation_pause()
- else:
- self.animation_start()
-
- def animation_one_stitch_forward(self, event):
- self.animation_pause()
- self.drawing_panel.one_stitch_forward()
-
- def animation_one_stitch_backward(self, event):
- self.animation_pause()
- self.drawing_panel.one_stitch_backward()
-
- def animation_one_command_backward(self, event):
- self.animation_pause()
- stitch_number = self.current_stitch - 1
- while stitch_number >= 1:
- # stitch number shown to the user starts at 1
- stitch = self.stitches[stitch_number - 1]
- if stitch.jump or stitch.trim or stitch.stop or stitch.color_change:
- break
- stitch_number -= 1
- self.drawing_panel.set_current_stitch(stitch_number)
-
- def animation_one_command_forward(self, event):
- self.animation_pause()
- stitch_number = self.current_stitch + 1
- while stitch_number <= self.num_stitches:
- # stitch number shown to the user starts at 1
- stitch = self.stitches[stitch_number - 1]
- if stitch.jump or stitch.trim or stitch.stop or stitch.color_change:
- break
- stitch_number += 1
- self.drawing_panel.set_current_stitch(stitch_number)
-
- def animation_restart(self, event):
- self.drawing_panel.restart()
-
- def on_toggle_npp_shortcut(self, event):
- self.btnNpp.SetValue(not self.btnNpp.GetValue())
- self.toggle_npp(event)
-
- def toggle_npp(self, event):
- self.drawing_panel.Refresh()
-
-
-class DrawingPanel(wx.Panel):
- """"""
-
- # render no faster than this many frames per second
- TARGET_FPS = 30
-
- # It's not possible to specify a line thickness less than 1 pixel, even
- # though we're drawing anti-aliased lines. To get around this we scale
- # the stitch positions up by this factor and then scale down by a
- # corresponding amount during rendering.
- PIXEL_DENSITY = 10
-
- # Line width in pixels.
- LINE_THICKNESS = 0.4
-
- def __init__(self, *args, **kwargs):
- """"""
- 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)
-
- # Drawing panel can really be any size, but without this wxpython likes
- # to allow the status bar and control panel to get squished.
- self.SetMinSize((100, 100))
- self.SetBackgroundColour('#FFFFFF')
- self.SetDoubleBuffered(True)
-
- self.animating = False
- self.target_frame_period = 1.0 / self.TARGET_FPS
- self.last_frame_duration = 0
- self.direction = 1
- self.current_stitch = 0
- self.black_pen = wx.Pen((128, 128, 128))
- self.width = 0
- self.height = 0
- self.loaded = False
-
- # desired simulation speed in stitches per second
- self.speed = 16
-
- self.Bind(wx.EVT_PAINT, self.OnPaint)
- self.Bind(wx.EVT_SIZE, self.choose_zoom_and_pan)
- self.Bind(wx.EVT_LEFT_DOWN, self.on_left_mouse_button_down)
- self.Bind(wx.EVT_MOUSEWHEEL, self.on_mouse_wheel)
- self.Bind(wx.EVT_SIZE, self.on_resize)
-
- # wait for layouts so that panel size is set
- if self.stitch_plan:
- wx.CallLater(50, self.load, self.stitch_plan)
-
- def on_resize(self, event):
- self.choose_zoom_and_pan()
- self.Refresh()
-
- def clamp_current_stitch(self):
- if self.current_stitch < 1:
- self.current_stitch = 1
- elif self.current_stitch > self.num_stitches:
- self.current_stitch = self.num_stitches
-
- def stop_if_at_end(self):
- if self.direction == -1 and self.current_stitch == 1:
- self.stop()
- elif self.direction == 1 and self.current_stitch == self.num_stitches:
- self.stop()
-
- def start_if_not_at_end(self):
- if self.direction == -1 and self.current_stitch > 1:
- self.go()
- elif self.direction == 1 and self.current_stitch < self.num_stitches:
- self.go()
-
- def animate(self):
- if not self.animating:
- return
-
- frame_time = max(self.target_frame_period, self.last_frame_duration)
-
- # No sense in rendering more frames per second than our desired stitches
- # per second.
- frame_time = max(frame_time, 1.0 / self.speed)
-
- stitch_increment = int(self.speed * frame_time)
-
- self.set_current_stitch(self.current_stitch + self.direction * stitch_increment)
- wx.CallLater(int(1000 * frame_time), self.animate)
-
- def OnPaint(self, e):
- dc = wx.PaintDC(self)
-
- if not self.loaded:
- dc.Clear()
- return
-
- canvas = wx.GraphicsContext.Create(dc)
-
- self.draw_stitches(canvas)
- self.draw_scale(canvas)
-
- def draw_stitches(self, canvas):
- canvas.BeginLayer(1)
-
- transform = canvas.GetTransform()
- transform.Translate(*self.pan)
- transform.Scale(self.zoom / self.PIXEL_DENSITY, self.zoom / self.PIXEL_DENSITY)
- canvas.SetTransform(transform)
-
- stitch = 0
- last_stitch = None
-
- start = time.time()
- for pen, stitches, jumps in zip(self.pens, self.stitch_blocks, self.jumps):
- canvas.SetPen(pen)
- if stitch + len(stitches) < self.current_stitch:
- stitch += len(stitches)
- if len(stitches) > 1:
- self.draw_stitch_lines(canvas, pen, stitches, jumps)
- self.draw_needle_penetration_points(canvas, pen, stitches)
- last_stitch = stitches[-1]
- else:
- stitches = stitches[:self.current_stitch - stitch]
- if len(stitches) > 1:
- self.draw_stitch_lines(canvas, pen, stitches, jumps)
- self.draw_needle_penetration_points(canvas, pen, stitches)
- last_stitch = stitches[-1]
- break
- self.last_frame_duration = time.time() - start
-
- if last_stitch:
- self.draw_crosshair(last_stitch[0], last_stitch[1], canvas, transform)
-
- canvas.EndLayer()
-
- def draw_crosshair(self, x, y, canvas, transform):
- x, y = transform.TransformPoint(float(x), float(y))
- canvas.SetTransform(canvas.CreateMatrix())
- crosshair_radius = 10
- canvas.SetPen(self.black_pen)
- canvas.StrokeLines(((x - crosshair_radius, y), (x + crosshair_radius, y)))
- canvas.StrokeLines(((x, y - crosshair_radius), (x, y + crosshair_radius)))
-
- def draw_scale(self, canvas):
- canvas.BeginLayer(1)
-
- canvas_width, canvas_height = self.GetClientSize()
-
- one_mm = PIXELS_PER_MM * self.zoom
- scale_width = one_mm
- max_width = min(canvas_width * 0.5, 300)
-
- while scale_width > max_width:
- scale_width /= 2.0
-
- while scale_width < 50:
- scale_width += one_mm
-
- scale_width_mm = int(scale_width / self.zoom / PIXELS_PER_MM)
-
- # The scale bar looks like this:
- #
- # | |
- # |_____|_____|
-
- scale_lower_left_x = 20
- scale_lower_left_y = canvas_height - 30
-
- canvas.StrokeLines(((scale_lower_left_x, scale_lower_left_y - 6),
- (scale_lower_left_x, scale_lower_left_y),
- (scale_lower_left_x + scale_width / 2.0, scale_lower_left_y),
- (scale_lower_left_x + scale_width / 2.0, scale_lower_left_y - 3),
- (scale_lower_left_x + scale_width / 2.0, scale_lower_left_y),
- (scale_lower_left_x + scale_width, scale_lower_left_y),
- (scale_lower_left_x + scale_width, scale_lower_left_y - 6)))
-
- canvas.SetFont(wx.Font(12, wx.DEFAULT, wx.NORMAL, wx.NORMAL), wx.Colour((0, 0, 0)))
- canvas.DrawText("%s mm" % scale_width_mm, scale_lower_left_x, scale_lower_left_y + 5)
-
- canvas.EndLayer()
-
- def draw_stitch_lines(self, canvas, pen, stitches, jumps):
- render_jumps = self.control_panel.btnJump.GetValue()
- if render_jumps:
- canvas.StrokeLines(stitches)
- else:
- stitch_blocks = split(stitches, jumps)
- for i, block in enumerate(stitch_blocks):
- if len(block) > 1:
- canvas.StrokeLines(block)
-
- def draw_needle_penetration_points(self, canvas, pen, stitches):
- if self.control_panel.btnNpp.GetValue():
- npp_pen = wx.Pen(pen.GetColour(), width=int(0.5 * PIXELS_PER_MM * self.PIXEL_DENSITY))
- canvas.SetPen(npp_pen)
- canvas.StrokeLineSegments(stitches, [(stitch[0] + 0.001, stitch[1]) for stitch in stitches])
-
- def clear(self):
- self.loaded = False
- self.Refresh()
-
- def load(self, stitch_plan):
- self.current_stitch = 1
- self.direction = 1
- self.last_frame_duration = 0
- self.minx, self.miny, self.maxx, self.maxy = stitch_plan.bounding_box
- self.width = self.maxx - self.minx
- self.height = self.maxy - self.miny
- self.num_stitches = stitch_plan.num_stitches
- self.parse_stitch_plan(stitch_plan)
- self.choose_zoom_and_pan()
- self.set_current_stitch(0)
- self.loaded = True
- self.go()
-
- def choose_zoom_and_pan(self, event=None):
- # ignore if EVT_SIZE fired before we load the stitch plan
- if not self.width and not self.height and event is not None:
- return
-
- panel_width, panel_height = self.GetClientSize()
-
- # add some padding to make stitches at the edge more visible
- width_ratio = panel_width / float(self.width + 10)
- height_ratio = panel_height / float(self.height + 10)
- self.zoom = max(min(width_ratio, height_ratio), 0.01)
-
- # center the design
- self.pan = ((panel_width - self.zoom * self.width) / 2.0,
- (panel_height - self.zoom * self.height) / 2.0)
-
- def stop(self):
- self.animating = False
- self.control_panel.on_stop()
-
- def go(self):
- if not self.loaded:
- return
-
- if not self.animating:
- self.animating = True
- self.animate()
- self.control_panel.on_start()
-
- def color_to_pen(self, color):
- # We draw the thread with a thickness of 0.1mm. Real thread has a
- # thickness of ~0.4mm, but if we did that, we wouldn't be able to
- # see the individual stitches.
- return wx.Pen(list(map(int, color.visible_on_white.rgb)), int(0.1 * PIXELS_PER_MM * self.PIXEL_DENSITY))
-
- def parse_stitch_plan(self, stitch_plan):
- self.pens = []
- self.stitch_blocks = []
- self.jumps = []
-
- # There is no 0th stitch, so add a place-holder.
- self.commands = [None]
-
- for color_block in stitch_plan:
- pen = self.color_to_pen(color_block.color)
- stitch_block = []
- jumps = []
- stitch_index = 0
-
- for stitch in color_block:
- # trim any whitespace on the left and top and scale to the
- # pixel density
- stitch_block.append((self.PIXEL_DENSITY * (stitch.x - self.minx),
- self.PIXEL_DENSITY * (stitch.y - self.miny)))
-
- if stitch.trim:
- self.commands.append(TRIM)
- elif stitch.jump:
- self.commands.append(JUMP)
- jumps.append(stitch_index)
- elif stitch.stop:
- self.commands.append(STOP)
- elif stitch.color_change:
- self.commands.append(COLOR_CHANGE)
- else:
- self.commands.append(STITCH)
-
- if stitch.trim or stitch.stop or stitch.color_change:
- self.pens.append(pen)
- self.stitch_blocks.append(stitch_block)
- stitch_block = []
- self.jumps.append(jumps)
- jumps = []
- stitch_index = 0
- else:
- stitch_index += 1
-
- if stitch_block:
- self.pens.append(pen)
- self.stitch_blocks.append(stitch_block)
- self.jumps.append(jumps)
-
- def set_speed(self, speed):
- self.speed = speed
-
- def forward(self):
- self.direction = 1
- self.start_if_not_at_end()
-
- def reverse(self):
- self.direction = -1
- self.start_if_not_at_end()
-
- def set_current_stitch(self, stitch):
- self.current_stitch = stitch
- self.clamp_current_stitch()
- 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], 1)
- self.stop_if_at_end()
- self.Refresh()
-
- def restart(self):
- if self.direction == 1:
- self.current_stitch = 1
- elif self.direction == -1:
- self.current_stitch = self.num_stitches
-
- self.go()
-
- def one_stitch_forward(self):
- self.set_current_stitch(self.current_stitch + 1)
-
- def one_stitch_backward(self):
- self.set_current_stitch(self.current_stitch - 1)
-
- def on_left_mouse_button_down(self, event):
- if self.loaded:
- self.CaptureMouse()
- self.drag_start = event.GetPosition()
- self.drag_original_pan = self.pan
- self.Bind(wx.EVT_MOTION, self.on_drag)
- self.Bind(wx.EVT_MOUSE_CAPTURE_LOST, self.on_drag_end)
- self.Bind(wx.EVT_LEFT_UP, self.on_drag_end)
-
- def on_drag(self, event):
- if self.HasCapture() and event.Dragging():
- delta = event.GetPosition()
- offset = (delta[0] - self.drag_start[0], delta[1] - self.drag_start[1])
- self.pan = (self.drag_original_pan[0] + offset[0], self.drag_original_pan[1] + offset[1])
- self.Refresh()
-
- def on_drag_end(self, event):
- if self.HasCapture():
- self.ReleaseMouse()
-
- self.Unbind(wx.EVT_MOTION)
- self.Unbind(wx.EVT_MOUSE_CAPTURE_LOST)
- self.Unbind(wx.EVT_LEFT_UP)
-
- def on_mouse_wheel(self, event):
- if event.GetWheelRotation() > 0:
- zoom_delta = 1.03
- else:
- zoom_delta = 0.97
-
- # If we just change the zoom, the design will appear to move on the
- # screen. We have to adjust the pan to compensate. We want to keep
- # the part of the design under the mouse pointer in the same spot
- # after we zoom, so that we appear to be zooming centered on the
- # mouse pointer.
-
- # This will create a matrix that takes a point in the design and
- # converts it to screen coordinates:
- matrix = wx.AffineMatrix2D()
- matrix.Translate(*self.pan)
- matrix.Scale(self.zoom, self.zoom)
-
- # First, figure out where the mouse pointer is in the coordinate system
- # of the design:
- pos = event.GetPosition()
- inverse_matrix = wx.AffineMatrix2D()
- inverse_matrix.Set(*matrix.Get())
- inverse_matrix.Invert()
- pos = inverse_matrix.TransformPoint(*pos)
-
- # Next, see how that point changes position on screen before and after
- # we apply the zoom change:
- x_old, y_old = matrix.TransformPoint(*pos)
- matrix.Scale(zoom_delta, zoom_delta)
- x_new, y_new = matrix.TransformPoint(*pos)
- x_delta = x_new - x_old
- y_delta = y_new - y_old
-
- # Finally, compensate for that change in position:
- self.pan = (self.pan[0] - x_delta, self.pan[1] - y_delta)
-
- self.zoom *= zoom_delta
-
- self.Refresh()
-
-
-class MarkerList(list):
- def __init__(self, icon_name, offset=0, stitch_numbers=()):
- super().__init__(self)
- icons_dir = get_resource_dir("icons")
- self.icon_name = icon_name
- self.icon = wx.Image(os.path.join(icons_dir, f"{icon_name}.png")).ConvertToBitmap()
- self.offset = offset
- self.enabled = False
- self.extend(stitch_numbers)
-
- def __repr__(self):
- return f"MarkerList({self.icon_name})"
-
-
-class ColorSection:
- def __init__(self, color, start, end):
- self.color = color
- self.start = start
- self.end = end
- self.brush = wx.Brush(wx.Colour(*color))
-
-
-class SimulatorSlider(wx.Panel):
- PROXY_EVENTS = (wx.EVT_SLIDER,)
-
- def __init__(self, parent, id=wx.ID_ANY, minValue=1, maxValue=2, **kwargs):
- super().__init__(parent, id)
- self.control_panel = parent
-
- kwargs['style'] = wx.SL_HORIZONTAL | wx.SL_VALUE_LABEL | wx.SL_TOP | wx.ALIGN_TOP
-
- self._height = self.GetTextExtent("M").y * 6
- self.SetMinSize((self._height, self._height))
-
- self.marker_lists = {
- "trim": MarkerList("trim"),
- "jump": MarkerList("jump", 0.17),
- "stop": MarkerList("stop", 0.34),
- "color_change": MarkerList("color_change", 0.34),
- }
- self.marker_pen = wx.Pen(wx.Colour(0, 0, 0))
- self.color_sections = []
- self.margin = 15
- self.tab_start = 0
- self.tab_width = 0.15
- self.tab_height = 0.15
- self.color_bar_start = 0.22
- self.color_bar_thickness = 0.17
- self.marker_start = self.color_bar_start
- self.marker_end = 0.5
- self.marker_icon_start = 0.5
- self.marker_icon_size = self._height // 6
-
- self._min = minValue
- self._max = maxValue
- self._value = 0
- self._tab_rect = None
-
- if sys.platform == "darwin":
- self.margin = 8
-
- self.Bind(wx.EVT_PAINT, self.on_paint)
- self.Bind(wx.EVT_ERASE_BACKGROUND, self.on_erase_background)
- self.Bind(wx.EVT_LEFT_DOWN, self.on_mouse_down)
- self.Bind(wx.EVT_LEFT_UP, self.on_mouse_up)
- self.Bind(wx.EVT_MOTION, self.on_mouse_motion)
-
- def SetMax(self, value):
- self._max = value
- self.Refresh()
-
- def SetMin(self, value):
- self._min = value
- self.Refresh()
-
- def SetValue(self, value):
- self._value = value
- self.Refresh()
-
- def GetValue(self):
- return self._value
-
- def clear(self):
- self.color_sections = []
- self._min = 1
- self._max = 2
- self._value = 0
- self._tab_rect = None
-
- for marker_list in self.marker_lists.values():
- marker_list.clear()
-
- def add_color_section(self, color, start, end):
- self.color_sections.append(ColorSection(color, start, end))
-
- def add_marker(self, name, location):
- self.marker_lists[name].append(location)
- self.Refresh()
-
- def enable_marker_list(self, name, enabled=True):
- self.marker_lists[name].enabled = enabled
- self.Refresh()
-
- def disable_marker_list(self, name):
- self.marker_lists[name].enabled = False
- self.Refresh()
-
- def toggle_marker_list(self, name):
- self.marker_lists[name].enabled = not self.marker_lists[name].enabled
- self.Refresh()
-
- def on_paint(self, event):
- dc = wx.BufferedPaintDC(self)
- if not sys.platform.startswith("win"):
- # Without this, the background color will be white.
- background_brush = wx.Brush(self.GetTopLevelParent().GetBackgroundColour(), wx.SOLID)
- dc.SetBackground(background_brush)
- dc.Clear()
- gc = wx.GraphicsContext.Create(dc)
-
- if self._value < self._min:
- return
-
- width, height = self.GetSize()
- min_value = self._min
- max_value = self._max
- spread = max_value - min_value
-
- def _value_to_x(value):
- return (value - min_value) * (width - 2 * self.margin) / spread + self.margin
-
- gc.SetPen(wx.NullPen)
- for color_section in self.color_sections:
- gc.SetBrush(color_section.brush)
-
- start_x = _value_to_x(color_section.start)
- end_x = _value_to_x(color_section.end)
- gc.DrawRectangle(start_x, height * self.color_bar_start,
- end_x - start_x, height * self.color_bar_thickness)
-
- if self.control_panel.is_dark_theme():
- gc.SetPen(wx.Pen(wx.Colour(0, 0, 0), 1))
- gc.SetBrush(wx.Brush(wx.Colour(255, 255, 255)))
- else:
- gc.SetPen(wx.Pen(wx.Colour(255, 255, 255), 1))
- gc.SetBrush(wx.Brush(wx.Colour(0, 0, 0)))
-
- value_x = _value_to_x(self._value)
- tab_height = self.tab_height * height
- tab_width = self.tab_width * height
- tab_x = value_x - tab_width / 2
- tab_y = height * self.tab_start
- self._tab_rect = wx.Rect(round(tab_x), round(tab_y), round(tab_width), round(tab_height))
- gc.DrawRectangle(
- value_x - 1.5, 0,
- 3, height * (self.color_bar_start + self.color_bar_thickness))
- gc.SetPen(wx.NullPen)
- gc.DrawRectangle(value_x - tab_width/2, height * self.tab_start,
- tab_width, tab_height)
-
- gc.SetPen(self.marker_pen)
- for marker_list in self.marker_lists.values():
- if marker_list.enabled:
- for value in marker_list:
- x = _value_to_x(value)
- gc.StrokeLine(
- x, height * self.marker_start,
- x, height * (self.marker_end + marker_list.offset)
- )
- gc.DrawBitmap(
- marker_list.icon,
- x - self.marker_icon_size / 2, height * (self.marker_icon_start + marker_list.offset),
- self.marker_icon_size, self.marker_icon_size
- )
-
- def on_erase_background(self, event):
- # supposedly this prevents flickering?
- pass
-
- def is_in_tab(self, point):
- return self._tab_rect and self._tab_rect.Inflate(2).Contains(point)
-
- def set_value_from_position(self, point):
- width, height = self.GetSize()
- min_value = self._min
- max_value = self._max
- spread = max_value - min_value
- value = round((point.x - self.margin) * spread / (width - 2 * self.margin))
- value = max(value, self._min)
- value = min(value, self._max)
- self.SetValue(round(value))
-
- event = wx.CommandEvent(wx.wxEVT_COMMAND_SLIDER_UPDATED, self.GetId())
- event.SetInt(value)
- event.SetEventObject(self)
- self.GetEventHandler().ProcessEvent(event)
-
- def on_mouse_down(self, event):
- click_pos = event.GetPosition()
- if self.is_in_tab(click_pos):
- debug.log("drag start")
- self.CaptureMouse()
- self.set_value_from_position(click_pos)
- self.Refresh()
- else:
- width, height = self.GetSize()
- relative_y = click_pos.y / height
- if relative_y > self.color_bar_start and relative_y - self.color_bar_start < self.color_bar_thickness:
- self.set_value_from_position(click_pos)
- self.Refresh()
-
- def on_mouse_motion(self, event):
- if self.HasCapture() and event.Dragging() and event.LeftIsDown():
- self.set_value_from_position(event.GetPosition())
- self.Refresh()
-
- def on_mouse_up(self, event):
- if self.HasCapture():
- self.ReleaseMouse()
- self.set_value_from_position(event.GetPosition())
- self.Refresh()
-
-
-class SimulatorPanel(wx.Panel):
- """"""
-
- def __init__(self, parent, stitch_plan=None, background_color='white', target_duration=5, stitches_per_second=16, detach_callback=None):
- """"""
- 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,
- detach_callback=detach_callback)
- self.dp = DrawingPanel(self, stitch_plan=stitch_plan, control_panel=self.cp)
- self.cp.set_drawing_panel(self.dp)
- self.cp.set_background_color(wx.Colour(background_color))
-
- vbSizer = wx.BoxSizer(wx.VERTICAL)
- vbSizer.Add(self.dp, 1, wx.EXPAND | wx.ALL, 2)
- vbSizer.Add(self.cp, 0, wx.EXPAND | wx.ALL, 2)
- self.SetSizerAndFit(vbSizer)
-
- def go(self):
- self.dp.go()
-
- def stop(self):
- self.dp.stop()
-
- def load(self, stitch_plan):
- self.dp.load(stitch_plan)
- self.cp.load(stitch_plan)
-
- def clear(self):
- self.dp.clear()
- self.cp.clear()
-
-
-class SimulatorWindow(wx.Frame):
- def __init__(self, panel=None, parent=None, **kwargs):
- background_color = kwargs.pop('background_color', 'white')
- super().__init__(None, title=_("Embroidery Simulation"), **kwargs)
-
- self.SetWindowStyle(wx.FRAME_FLOAT_ON_PARENT | wx.DEFAULT_FRAME_STYLE)
-
- self.sizer = wx.BoxSizer(wx.VERTICAL)
-
- self.statusbar = self.CreateStatusBar(2)
- self.statusbar.SetStatusWidths((0, -1))
-
- 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.panel = SimulatorPanel(self, background_color=background_color)
- self.sizer.Add(self.panel, 1, wx.EXPAND)
-
- self.SetSizer(self.sizer)
- self.Layout()
-
- self.SetMinSize(self.sizer.CalcMin())
-
- if self.is_child:
- self.Bind(wx.EVT_CLOSE, self.on_close)
- else:
- self.Maximize()
-
- def detach_simulator_panel(self):
- self.sizer.Detach(self.panel)
-
- def on_close(self, event):
- self.parent.attach_simulator()
-
- def load(self, stitch_plan):
- self.panel.load(stitch_plan)
-
- def go(self):
- self.panel.go()
-
-
-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.statusbar = self.CreateStatusBar(2)
-
- self.detached_simulator_frame = None
- self.splitter = wx.SplitterWindow(self, style=wx.SP_LIVE_UPDATE)
- background_color = kwargs.pop('background_color', 'white')
- self.cancel_hook = kwargs.pop('on_cancel', None)
- self.simulator_panel = SimulatorPanel(
- self.splitter,
- background_color=background_color,
- 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.sizer = wx.BoxSizer(wx.VERTICAL)
- self.sizer.Add(self.splitter, 1, wx.EXPAND)
- self.SetSizer(self.sizer)
-
- self.SetMinSize(self.sizer.CalcMin())
-
- self.simulator_panel.SetFocus()
- 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.cancel)
-
- 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 cancel(self, event=None):
- if self.cancel_hook:
- self.cancel_hook()
- self.close(None)
-
- def close(self, event=None):
- self.simulator_panel.stop()
- if self.detached_simulator_frame:
- self.detached_simulator_frame.Destroy()
- self.Destroy()
-
- def toggle_detach_simulator(self):
- if self.detached_simulator_frame:
- self.attach_simulator()
- else:
- self.detach_simulator()
-
- 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)
-
- self.GetStatusBar().SetStatusText(self.detached_simulator_frame.GetStatusBar().GetStatusText(1), 1)
-
- self.detached_simulator_frame.Destroy()
- self.detached_simulator_frame = None
- self.Maximize()
- self.splitter.UpdateSize()
- self.simulator_panel.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 PreviewRenderer(Thread):
- """Render stitch plan in a background thread."""
-
- def __init__(self, render_stitch_plan_hook, rendering_completed_hook):
- super(PreviewRenderer, self).__init__()
- self.daemon = True
- 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()
-
- def update(self):
- """Request to render a new stitch plan.
-
- 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()
-
- self.stop.set()
- self.refresh_needed.set()
-
- def run(self):
- while True:
- self.refresh_needed.wait()
- self.refresh_needed.clear()
- self.stop.clear()
-
- try:
- debug.log("update_patches")
- self.render_stitch_plan()
- except ExitThread:
- debug.log("ExitThread caught")
- self.stop.clear()
-
- def render_stitch_plan(self):
- try:
- 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
- import traceback
- debug.log("unhandled exception in PreviewRenderer.render_stitch_plan(): " + traceback.format_exc())