# 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 sys import time from threading import Event, Thread import wx from wx.lib.intctrl import IntCtrl from ..i18n import _ from ..stitch_plan import stitch_groups_to_stitch_plan, stitch_plan_from_file 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): """""" def __init__(self, parent, *args, **kwargs): """""" self.parent = parent self.stitch_plan = kwargs.pop('stitch_plan') 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.statusbar = self.GetTopLevelParent().statusbar self.drawing_panel = None self.num_stitches = 1 self.current_stitch = 1 self.speed = 1 self.direction = 1 # Widgets self.btnMinus = wx.Button(self, -1, label='-') self.btnMinus.Bind(wx.EVT_BUTTON, self.animation_slow_down) self.btnMinus.SetToolTip(_('Slow down (arrow down)')) self.btnPlus = wx.Button(self, -1, label='+') self.btnPlus.Bind(wx.EVT_BUTTON, self.animation_speed_up) self.btnPlus.SetToolTip(_('Speed up (arrow up)')) self.btnBackwardStitch = wx.Button(self, -1, label='<|') self.btnBackwardStitch.Bind(wx.EVT_BUTTON, self.animation_one_stitch_backward) self.btnBackwardStitch.SetToolTip(_('Go on step backward (-)')) self.btnForwardStitch = wx.Button(self, -1, label='|>') self.btnForwardStitch.Bind(wx.EVT_BUTTON, self.animation_one_stitch_forward) self.btnForwardStitch.SetToolTip(_('Go on step forward (+)')) self.directionBtn = wx.Button(self, -1, label='<<') self.directionBtn.Bind(wx.EVT_BUTTON, self.on_direction_button) self.directionBtn.SetToolTip(_('Switch direction (arrow left | arrow right)')) self.pauseBtn = wx.Button(self, -1, label=_('Pause')) self.pauseBtn.Bind(wx.EVT_BUTTON, self.on_pause_start_button) self.pauseBtn.SetToolTip(_('Pause (P)')) self.restartBtn = wx.Button(self, -1, label=_('Restart')) self.restartBtn.Bind(wx.EVT_BUTTON, self.animation_restart) self.restartBtn.SetToolTip(_('Restart (R)')) self.nppBtn = wx.ToggleButton(self, -1, label=_('O')) self.nppBtn.Bind(wx.EVT_TOGGLEBUTTON, self.toggle_npp) self.nppBtn.SetToolTip(_('Display needle penetration point (O)')) self.quitBtn = wx.Button(self, -1, label=_('Quit')) self.quitBtn.Bind(wx.EVT_BUTTON, self.animation_quit) self.quitBtn.SetToolTip(_('Quit (Q)')) self.slider = wx.Slider(self, -1, value=1, minValue=1, maxValue=2, style=wx.SL_HORIZONTAL | wx.SL_LABELS) 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, 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) # Layout self.vbSizer = vbSizer = wx.BoxSizer(wx.VERTICAL) self.hbSizer1 = hbSizer1 = wx.BoxSizer(wx.HORIZONTAL) self.hbSizer2 = hbSizer2 = wx.BoxSizer(wx.HORIZONTAL) hbSizer1.Add(self.slider, 1, wx.EXPAND | wx.ALL, 3) hbSizer1.Add(self.stitchBox, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 2) vbSizer.Add(hbSizer1, 1, wx.EXPAND | wx.ALL, 3) hbSizer2.Add(self.btnMinus, 0, wx.EXPAND | wx.ALL, 2) hbSizer2.Add(self.btnPlus, 0, wx.EXPAND | wx.ALL, 2) hbSizer2.Add(self.btnBackwardStitch, 0, wx.EXPAND | wx.ALL, 2) hbSizer2.Add(self.btnForwardStitch, 0, wx.EXPAND | wx.ALL, 2) hbSizer2.Add(self.directionBtn, 0, wx.EXPAND | wx.ALL, 2) hbSizer2.Add(self.pauseBtn, 0, wx.EXPAND | wx.ALL, 2) hbSizer2.Add(self.restartBtn, 0, wx.EXPAND | wx.ALL, 2) hbSizer2.Add(self.nppBtn, 0, wx.EXPAND | wx.ALL, 2) hbSizer2.Add(self.quitBtn, 0, wx.EXPAND | wx.ALL, 2) vbSizer.Add(hbSizer2, 0, wx.EXPAND | wx.ALL, 3) 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.on_pause_start_button), (wx.ACCEL_NORMAL, wx.WXK_SPACE, self.on_pause_start_button), (wx.ACCEL_NORMAL, ord('q'), self.animation_quit)] 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) self.SetFocus() 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.SetMax(num_stitches) self.slider.SetMax(num_stitches) self.choose_speed() 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.directionBtn.SetLabel("<<") self.drawing_panel.forward() self.direction = 1 self.update_speed_text() def animation_reverse(self, event=None): self.directionBtn.SetLabel(">>") self.drawing_panel.reverse() self.direction = -1 self.update_speed_text() def on_direction_button(self, event): if self.direction == 1: self.animation_reverse() else: self.animation_forward() 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 update_speed_text(self): self.statusbar.SetStatusText(_('Speed: %d stitches/sec') % (self.speed * self.direction), 0) self.hbSizer2.Layout() def on_slider(self, event): 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) self.statusbar.SetStatusText(COMMAND_NAMES[command], 1) 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() self.parent.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) 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.pauseBtn.SetLabel(_('Pause')) def on_stop(self): self.pauseBtn.SetLabel(_('Start')) def on_pause_start_button(self, event): """""" if self.pauseBtn.GetLabel() == _('Pause'): 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_quit(self, event): self.parent.quit() def animation_restart(self, event): self.drawing_panel.restart() def on_toggle_npp_shortcut(self, event): self.nppBtn.SetValue(not self.nppBtn.GetValue()) self.toggle_npp(event) def toggle_npp(self, event): if self.pauseBtn.GetLabel() == _('Start'): stitch = self.stitchBox.GetValue() self.drawing_panel.set_current_stitch(stitch) 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') 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) # wait for layouts so that panel size is set wx.CallLater(50, self.load, self.stitch_plan) 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): if not self.loaded: return dc = wx.PaintDC(self) 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 in zip(self.pens, self.stitch_blocks): canvas.SetPen(pen) if stitch + len(stitches) < self.current_stitch: stitch += len(stitches) if len(stitches) > 1: canvas.StrokeLines(stitches) self.draw_needle_penetration_points(canvas, pen, stitches) last_stitch = stitches[-1] else: stitches = stitches[:self.current_stitch - stitch] if len(stitches) > 1: canvas.StrokeLines(stitches) 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.DrawLines(((x - crosshair_radius, y), (x + crosshair_radius, y))) canvas.DrawLines(((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.DrawLines(((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 - 5))) 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_needle_penetration_points(self, canvas, pen, stitches): if self.control_panel.nppBtn.GetValue(): npp_pen = wx.Pen(pen.GetColour(), width=int(0.3 * PIXELS_PER_MM * self.PIXEL_DENSITY)) canvas.SetPen(npp_pen) canvas.StrokeLineSegments(stitches, stitches) def clear(self): dc = wx.ClientDC(self) dc.Clear() def load(self, stitch_plan): self.current_stitch = 1 self.direction = 1 self.last_frame_duration = 0 self.num_stitches = stitch_plan.num_stitches self.control_panel.set_num_stitches(self.num_stitches) 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.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 = min(width_ratio, height_ratio) # 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 = [] # 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 = [] 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) 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 = [] if stitch_block: self.pens.append(pen) self.stitch_blocks.append(stitch_block) 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() self.control_panel.on_current_stitch(self.current_stitch, self.commands[self.current_stitch]) 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): 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 SimulatorPanel(wx.Panel): """""" def __init__(self, parent, *args, **kwargs): """""" 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) self.cp = ControlPanel(self, stitch_plan=stitch_plan, stitches_per_second=stitches_per_second, target_duration=target_duration) self.dp = DrawingPanel(self, stitch_plan=stitch_plan, control_panel=self.cp) self.cp.set_drawing_panel(self.dp) 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 quit(self): self.parent.quit() def go(self): self.dp.go() def stop(self): self.dp.stop() def load(self, stitch_plan): self.dp.load(stitch_plan) def clear(self): 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) self.statusbar = self.CreateStatusBar(2) self.statusbar.SetStatusWidths([250, -1]) 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.SetSizeHints(sizer.CalcMin()) self.Bind(wx.EVT_CLOSE, self.on_close) def quit(self): self.Close() def on_close(self, event): self.simulator_panel.stop() if self.on_close_hook: self.on_close_hook() self.SetFocus() self.Destroy() def go(self): self.simulator_panel.go() def stop(self): self.simulator_panel.stop() def load(self, stitch_plan): self.simulator_panel.load(stitch_plan) def clear(self): self.simulator_panel.clear() class SimulatorPreview(Thread): """Manages a preview simulation and a background thread for generating patches.""" 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) self.daemon = True self.simulate_window = None self.refresh_needed = 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.""" if self.simulate_window: self.simulate_window.stop() self.simulate_window.clear() if self._disabled: return if not self.is_alive(): self.start() self.refresh_needed.set() def run(self): while True: self.refresh_needed.wait() self.refresh_needed.clear() self.update_patches() def update_patches(self): try: patches = self.parent.generate_patches(self.refresh_needed) 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(): stitch_plan = stitch_groups_to_stitch_plan(patches) # 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: 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() def show_simulator(stitch_plan): app = wx.App() current_screen = wx.Display.GetFromPoint(wx.GetMousePosition()) display = wx.Display(current_screen) screen_rect = display.GetClientArea() simulator_pos = (screen_rect[0], screen_rect[1]) # subtract 1 because otherwise the window becomes maximized on Linux 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) app.SetTopWindow(frame) frame.Show() app.MainLoop() if __name__ == "__main__": stitch_plan = stitch_plan_from_file(sys.argv[1]) show_simulator(stitch_plan)