summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--lib/extensions/params.py15
-rw-r--r--lib/extensions/simulate.py18
-rw-r--r--lib/output.py8
-rw-r--r--lib/simulator.py806
-rw-r--r--lib/stitch_plan/__init__.py3
-rw-r--r--lib/stitch_plan/read_file.py21
-rw-r--r--lib/stitch_plan/stitch_plan.py6
-rw-r--r--messages.po88
8 files changed, 638 insertions, 327 deletions
diff --git a/lib/extensions/params.py b/lib/extensions/params.py
index 6012263a..784b6be7 100644
--- a/lib/extensions/params.py
+++ b/lib/extensions/params.py
@@ -293,7 +293,6 @@ class ParamsTab(ScrolledPanel):
summary_box = wx.StaticBox(self, wx.ID_ANY, label=_("Inkscape objects"))
sizer = wx.StaticBoxSizer(summary_box, wx.HORIZONTAL)
-# sizer = wx.BoxSizer(wx.HORIZONTAL)
self.description = wx.StaticText(self)
self.update_description()
self.description.SetLabel(self.description_text)
@@ -451,7 +450,7 @@ class SettingsFrame(wx.Frame):
stitch_plan = patches_to_stitch_plan(patches)
if self.simulate_window:
self.simulate_window.stop()
- self.simulate_window.load(stitch_plan=stitch_plan)
+ self.simulate_window.load(stitch_plan)
else:
params_rect = self.GetScreenRect()
simulator_pos = params_rect.GetTopRight()
@@ -460,20 +459,18 @@ class SettingsFrame(wx.Frame):
current_screen = wx.Display.GetFromPoint(wx.GetMousePosition())
display = wx.Display(current_screen)
screen_rect = display.GetClientArea()
+ simulator_pos.y = screen_rect.GetTop()
- max_width = screen_rect.GetWidth() - params_rect.GetWidth()
- max_height = screen_rect.GetHeight()
+ width = screen_rect.GetWidth() - params_rect.GetWidth()
+ height = screen_rect.GetHeight()
try:
self.simulate_window = EmbroiderySimulator(None, -1, _("Preview"),
simulator_pos,
- size=(300, 300),
- x_position=simulator_pos.x,
+ size=(width, height),
stitch_plan=stitch_plan,
on_close=self.simulate_window_closed,
- target_duration=5,
- max_width=max_width,
- max_height=max_height)
+ target_duration=5)
except:
error = traceback.format_exc()
diff --git a/lib/extensions/simulate.py b/lib/extensions/simulate.py
index 38f86156..f962b206 100644
--- a/lib/extensions/simulate.py
+++ b/lib/extensions/simulate.py
@@ -1,8 +1,5 @@
-import wx
-
from .base import InkstitchExtension
-from ..i18n import _
-from ..simulator import EmbroiderySimulator
+from ..simulator import show_simulator
from ..stitch_plan import patches_to_stitch_plan
@@ -19,15 +16,4 @@ class Simulate(InkstitchExtension):
return
patches = self.elements_to_patches(self.elements)
stitch_plan = patches_to_stitch_plan(patches)
- 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])
-
- frame = EmbroiderySimulator(None, -1, _("Embroidery Simulation"), pos=simulator_pos, size=(1000, 1000), stitch_plan=stitch_plan)
- app.SetTopWindow(frame)
- frame.Show()
- wx.CallAfter(frame.go)
- app.MainLoop()
+ show_simulator(stitch_plan)
diff --git a/lib/output.py b/lib/output.py
index cf7895e8..ed97ef04 100644
--- a/lib/output.py
+++ b/lib/output.py
@@ -86,6 +86,14 @@ def write_embroidery_file(file_path, stitch_plan, svg):
"full_jump": True,
}
+ if file_path.endswith('.csv'):
+ # Special treatment for CSV: instruct pyembroidery not to do any post-
+ # processing. This will allow the user to match up stitch numbers seen
+ # in the simulator with commands in the CSV.
+ settings['max_stitch'] = float('inf')
+ settings['max_jump'] = float('inf')
+ settings['explicit_trim'] = False
+
try:
pyembroidery.write(pattern, file_path, settings)
except IOError as e:
diff --git a/lib/simulator.py b/lib/simulator.py
index 3df72a08..4408adb8 100644
--- a/lib/simulator.py
+++ b/lib/simulator.py
@@ -1,88 +1,110 @@
import sys
-import numpy
import wx
-import colorsys
+from wx.lib.intctrl import IntCtrl
+import time
from itertools import izip
-from .svg import PIXELS_PER_MM, color_block_to_point_lists
+from .svg import PIXELS_PER_MM
from .i18n import _
-
-class EmbroiderySimulator(wx.Frame):
- def __init__(self, *args, **kwargs):
- stitch_plan = kwargs.pop('stitch_plan', None)
- self.x_position = kwargs.pop('x_position', None)
- self.on_close_hook = kwargs.pop('on_close', None)
- self.frame_period = kwargs.pop('frame_period', 80)
- self.stitches_per_frame = kwargs.pop('stitches_per_frame', 1)
- self.target_duration = kwargs.pop('target_duration', None)
-
- self.margin = 30
-
- screen_rect = self.get_current_screen_rect()
- self.max_width = kwargs.pop('max_width', screen_rect[2])
- self.max_height = kwargs.pop('max_height', screen_rect[3])
- self.scale = 1
-
- self.min_width = 600
- if self.max_width < self.min_width:
- self.max_width = self.min_width
-
- wx.Frame.__init__(self, *args, **kwargs)
-
- self.panel = wx.Panel(self, wx.ID_ANY)
-
- self.panel.SetDoubleBuffered(True)
-
- self.sizer = wx.BoxSizer(wx.VERTICAL)
- self.button_sizer = wx.BoxSizer(wx.HORIZONTAL)
-
- self.button_label = (
- [_("Speed up"), _('Press + or arrow up to speed up'), self.animation_speed_up],
- [_("Slow down"), _('Press - or arrow down to slow down'), self.animation_slow_down],
- [_("Pause"), _("Press P to pause the animation"), self.animation_pause],
- [_("Restart"), _("Press R to restart the animation"), self.animation_restart],
- [_("Quit"), _("Press Q to close the simulation window"), self.animation_quit])
- self.buttons = []
- for i in range(0, len(self.button_label)):
- self.buttons.append(wx.Button(self, -1, self.button_label[i][0]))
- self.button_sizer.Add(self.buttons[i], 1, wx.EXPAND)
- self.buttons[i].SetToolTip(self.button_label[i][1])
- self.buttons[i].Bind(wx.EVT_BUTTON, self.button_label[i][2])
-
- self.sizer.Add(self.panel, 1, wx.EXPAND)
- self.sizer.Add(self.button_sizer, 0, wx.EXPAND)
- self.SetSizer(self.sizer)
-
- self.load(stitch_plan)
-
- if self.target_duration:
- self.adjust_speed(self.target_duration)
-
- self.buffer = wx.Bitmap(self.width * self.scale + self.margin * 2, self.height * self.scale + self.margin * 2)
- self.dc = wx.MemoryDC()
- self.dc.SelectObject(self.buffer)
- self.canvas = wx.GraphicsContext.Create(self.dc)
-
- self.clear()
-
- self.set_stitch_counter(1)
-
+from .stitch_plan import stitch_plan_from_file
+
+# 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.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.btnPlus = wx.Button(self, -1, label='+')
+ self.btnPlus.Bind(wx.EVT_BUTTON, self.animation_speed_up)
+ self.btnBackwardStitch = wx.Button(self, -1, label='<|')
+ self.btnBackwardStitch.Bind(wx.EVT_BUTTON, self.animation_one_stitch_backward)
+ self.btnForwardStitch = wx.Button(self, -1, label='|>')
+ self.btnForwardStitch.Bind(wx.EVT_BUTTON, self.animation_one_stitch_forward)
+ self.directionBtn = wx.Button(self, -1, label='<<')
+ self.directionBtn.Bind(wx.EVT_BUTTON, self.on_direction_button)
+ self.pauseBtn = wx.Button(self, -1, label=_('Pause'))
+ self.pauseBtn.Bind(wx.EVT_BUTTON, self.on_pause_start_button)
+ self.restartBtn = wx.Button(self, -1, label=_('Restart'))
+ self.restartBtn.Bind(wx.EVT_BUTTON, self.animation_restart)
+ self.quitBtn = wx.Button(self, -1, label=_('Quit'))
+ self.quitBtn.Bind(wx.EVT_BUTTON, self.animation_quit)
+ 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=False)
+ self.stitchBox.Bind(wx.EVT_TEXT, self.on_stitch_box)
+ self.speedST = wx.StaticText(self, -1, label='', style=wx.ALIGN_CENTER)
+ self.commandST = wx.StaticText(self, -1, label='', style=wx.ALIGN_CENTER)
+
+ # 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.speedST, 0, wx.ALIGN_CENTER_VERTICAL | wx.ALL, 2)
+ hbSizer2.AddStretchSpacer(prop=1)
+ hbSizer2.Add(self.commandST, 0, wx.ALIGN_CENTER_VERTICAL | wx.ALL, 2)
+ hbSizer2.AddStretchSpacer(prop=1)
+ hbSizer2.Add(self.btnMinus, 0, wx.ALL, 2)
+ hbSizer2.Add(self.btnPlus, 0, wx.ALL, 2)
+ hbSizer2.Add(self.btnBackwardStitch, 0, wx.ALL, 2)
+ hbSizer2.Add(self.btnForwardStitch, 0, 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.quitBtn, 0, wx.EXPAND | wx.ALL, 2)
+ vbSizer.Add(hbSizer2, 0, wx.EXPAND | wx.ALL, 3)
+ self.SetSizer(vbSizer)
+
+ # Keyboard Shortcuts
shortcut_keys = [
- (wx.ACCEL_NORMAL, ord('+'), self.animation_speed_up),
- (wx.ACCEL_NORMAL, ord('='), self.animation_speed_up),
- (wx.ACCEL_SHIFT, ord('='), self.animation_speed_up),
- (wx.ACCEL_NORMAL, wx.WXK_ADD, self.animation_speed_up),
- (wx.ACCEL_NORMAL, wx.WXK_NUMPAD_ADD, self.animation_speed_up),
- (wx.ACCEL_NORMAL, wx.WXK_NUMPAD_UP, self.animation_speed_up),
+ (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, ord('-'), self.animation_slow_down),
- (wx.ACCEL_NORMAL, ord('_'), self.animation_slow_down),
- (wx.ACCEL_NORMAL, wx.WXK_SUBTRACT, self.animation_slow_down),
- (wx.ACCEL_NORMAL, wx.WXK_NUMPAD_SUBTRACT, self.animation_slow_down),
- (wx.ACCEL_NORMAL, wx.WXK_NUMPAD_DOWN, self.animation_slow_down),
+ (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('p'), self.animation_pause),
+ (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)]
accel_entries = []
@@ -94,250 +116,526 @@ class EmbroiderySimulator(wx.Frame):
accel_table = wx.AcceleratorTable(accel_entries)
self.SetAcceleratorTable(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):
+ evtObj = event.GetEventObject()
+ lbl = evtObj.GetLabel()
+ if self.direction == 1:
+ self.animation_forward()
+ else:
+ self.animation_reverse()
- self.Bind(wx.EVT_SIZE, self.on_size)
- self.Bind(wx.EVT_CLOSE, self.on_close)
- self.panel.Bind(wx.EVT_PAINT, self.on_paint)
+ def set_speed(self, speed):
+ self.speed = int(max(speed, 1))
+ self.update_speed_text()
- self.panel.SetFocus()
+ if self.drawing_panel:
+ self.drawing_panel.set_speed(self.speed)
- self.timer = None
+ def update_speed_text(self):
+ self.speedST.SetLabel(_('Speed: %d stitches/sec') % (self.speed * self.direction))
+ self.hbSizer2.Layout()
- self.last_pos = None
- def get_current_screen_rect(self):
- current_screen = wx.Display.GetFromPoint(wx.GetMousePosition())
- display = wx.Display(current_screen)
- screen_rect = display.GetClientArea()
- return screen_rect
+ def on_slider(self, event):
+ stitch = event.GetEventObject().GetValue()
+ self.stitchBox.SetValue(stitch)
- def load(self, stitch_plan=None):
- if stitch_plan:
- self.mirror = False
- self.segments = self._stitch_plan_to_segments(stitch_plan)
- else:
- return
+ if self.drawing_panel:
+ self.drawing_panel.set_current_stitch(stitch)
- self.trim_margins()
- self.calculate_dimensions()
+ 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.commandST.SetLabel(COMMAND_NAMES[command])
- def adjust_speed(self, duration):
- self.frame_period = 1000 * float(duration) / len(self.segments)
- self.stitches_per_frame = 1
+ def on_stitch_box(self, event):
+ stitch = self.stitchBox.GetValue()
+ self.slider.SetValue(stitch)
- while self.frame_period < 1.0:
- self.frame_period *= 2
- self.stitches_per_frame *= 2
+ 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):
- if self.frame_period == 1:
- self.stitches_per_frame *= 2
- else:
- self.frame_period = self.frame_period / 2
- self.animation_update_timer()
+ """"""
+ self.set_speed(self.speed * 2.0)
- def animation_slow_down(self, event):
- if self.stitches_per_frame == 1:
- self.frame_period *= 2
- else:
- self.stitches_per_frame /= 2
- self.animation_update_timer()
+ def animation_pause(self, event=None):
+ self.drawing_panel.stop()
- def animation_restart(self, event):
- self.stop()
- self.clear()
- self.go()
+ 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 animation_pause(self, event):
- if self.timer.IsRunning():
- self.timer.Stop()
+ def on_pause_start_button(self, event):
+ """"""
+ if self.pauseBtn.GetLabel() == _('Pause'):
+ self.animation_pause()
else:
- self.timer.Start(self.frame_period)
+ self.animation_start()
+
+ def animation_one_stitch_forward(self, event):
+ self.drawing_panel.one_stitch_forward()
+
+ def animation_one_stitch_backward(self, event):
+ self.drawing_panel.one_stitch_backward()
def animation_quit(self, event):
- self.Close()
+ self.parent.quit()
- def animation_update_timer(self):
- self.frame_period = max(1, self.frame_period)
- self.stitches_per_frame = max(self.stitches_per_frame, 1)
- if self.timer.IsRunning():
- self.timer.Stop()
- self.timer.Start(self.frame_period)
+ def animation_restart(self, event):
+ self.drawing_panel.restart()
- def set_stitch_counter(self, current_stitch):
- if hasattr(self.panel, 'stitch_counter'):
- self.panel.stitch_counter.SetLabel(_("Stitch # ") + str(current_stitch) + ' / ' + str(len(self.segments) + 1))
- else:
- self.font = wx.Font(9, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL)
- self.panel.stitch_counter = wx.StaticText(self, label=_("Stitch #") + '1 / ' + str(len(self.segments)), pos=(30, 10))
- self.panel.stitch_counter.SetFont(self.font)
- self.panel.stitch_counter.SetForegroundColour('red')
- self.panel.stitch_counter.SetBackgroundColour('white')
+class DrawingPanel(wx.Panel):
+ """"""
- def _strip_quotes(self, string):
- if string.startswith('"') and string.endswith('"'):
- string = string[1:-1]
+ # render no faster than this many frames per second
+ TARGET_FPS = 30
- return string
+ # 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
- def color_to_pen(self, color):
- return wx.Pen(color.visible_on_white.rgb)
+ # Line width in pixels.
+ LINE_THICKNESS = 0.4
- def _stitch_plan_to_segments(self, stitch_plan):
- segments = []
+ 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)
+
+ 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
- for color_block in stitch_plan:
- pen = self.color_to_pen(color_block.color)
+ frame_time = max(self.target_frame_period, self.last_frame_duration)
- for point_list in color_block_to_point_lists(color_block):
- # if there's only one point, there's nothing to do, so skip
- if len(point_list) < 2:
- continue
+ # No sense in rendering more frames per second than our desired stitches
+ # per second.
+ frame_time = max(frame_time, 1.0 / self.speed)
- for start, end in izip(point_list[:-1], point_list[1:]):
- segments.append(((start, end), pen))
+ stitch_increment = int(self.speed * frame_time)
- return segments
+ #print >> sys.stderr, time.time(), "animate", self.current_stitch, stitch_increment, self.last_frame_duration, frame_time
- def all_coordinates(self):
- for segment in self.segments:
- start, end = segment[0]
+ self.set_current_stitch(self.current_stitch + self.direction * stitch_increment)
+ wx.CallLater(int(1000 * frame_time), self.animate)
- yield start
- yield end
+ def OnPaint(self, e):
+ if not self.loaded:
+ return
- def trim_margins(self):
- """remove any unnecessary whitespace around the design"""
+ dc = wx.PaintDC(self)
+ canvas = wx.GraphicsContext.Create(dc)
+
+ transform = canvas.GetTransform()
+ transform.Translate(*self.pan)
+ transform.Scale(self.zoom / self.PIXEL_DENSITY, self.zoom / self.PIXEL_DENSITY)
+ #transform.Translate(self.pan[0] * self.PIXEL_DENSITY, self.pan[1] * self.PIXEL_DENSITY)
+ canvas.SetTransform(transform)
+
+ stitch = 0
+ last_stitch = None
+
+ start = time.time()
+ for pen, stitches in izip(self.pens, self.stitch_blocks):
+ canvas.SetPen(pen)
+ if stitch + len(stitches) < self.current_stitch:
+ stitch += len(stitches)
+ if len(stitches) > 1:
+ canvas.DrawLines(stitches)
+ last_stitch = stitches[-1]
+ else:
+ stitches = stitches[:self.current_stitch - stitch]
+ if len(stitches) > 1:
+ canvas.DrawLines(stitches)
+ last_stitch = stitches[-1]
+ break
+ self.last_frame_duration = time.time() - start
+
+ if last_stitch:
+ x = last_stitch[0]
+ y = last_stitch[1]
+ 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)))
- min_x = sys.maxint
- min_y = sys.maxint
+ 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()
- for x, y in self.all_coordinates():
- min_x = min(min_x, x)
- min_y = min(min_y, y)
+ def choose_zoom_and_pan(self, event=None):
+ # ignore if called before we load the stitch plan
+ if not self.width and not self.height:
+ return
- new_segments = []
+ panel_width, panel_height = self.GetClientSize()
- for segment in self.segments:
- (start, end), color = segment
+ # 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)
- new_segment = (
- (
- (start[0] - min_x, start[1] - min_y),
- (end[0] - min_x, end[1] - min_y),
- ),
- color
- )
+ # center the design
+ self.pan = ((panel_width - self.zoom * self.width) / 2.0,
+ (panel_height - self.zoom * self.height) / 2.0)
- new_segments.append(new_segment)
+ def stop(self):
+ self.animating = False
+ self.control_panel.on_stop()
- self.segments = new_segments
+ def go(self):
+ if not self.loaded:
+ return
- def calculate_dimensions(self):
- # 0.01 avoids a division by zero below for designs with no width or
- # height (e.g. a straight vertical or horizontal line)
- width = 0.01
- height = 0.01
+ if not self.animating:
+ self.animating = True
+ self.animate()
+ self.control_panel.on_start()
- for x, y in self.all_coordinates():
- width = max(width, x)
- height = max(height, y)
+ 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(color.visible_on_white.rgb, width=int(0.1 * PIXELS_PER_MM * self.PIXEL_DENSITY))
- self.width = width
- self.height = height
- self.scale = min(float(self.max_width - self.margin * 2) / width, float(self.max_height - self.margin * 2 - 40) / height)
+ def parse_stitch_plan(self, stitch_plan):
+ self.pens = []
+ self.stitch_blocks = []
- # make room for decorations and the margin
- self.scale *= 0.95
+ # There is no 0th stitch, so add a place-holder.
+ self.commands = [None]
- def go(self):
- self.clear()
+ 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()
- self.current_stitch = 0
+ def restart(self):
+ if self.direction == 1:
+ self.current_stitch = 1
+ elif self.direction == -1:
+ self.current_stitch = self.num_stitches
- if not self.timer:
- self.timer = wx.PyTimer(self.draw_one_frame)
+ self.go()
- self.timer.Start(self.frame_period)
+ 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
- def on_close(self, event):
- self.stop()
+ # 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 appar to be zooming centered on the
+ # mouse pointer.
- if self.on_close_hook:
- self.on_close_hook()
+ # 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)
- # If we keep a reference here, wx crashes when the process exits.
- self.canvas = None
+ # 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.Destroy()
+
+ 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.SetSizer(vbSizer)
+
+ def quit(self):
+ self.parent.quit()
+
+ def go(self):
+ self.dp.go()
def stop(self):
- if self.timer:
- self.timer.Stop()
+ self.dp.stop()
+
+ def load(self, stitch_plan):
+ self.dp.load(stitch_plan)
def clear(self):
- self.dc.SetBackground(wx.Brush('white'))
- self.dc.Clear()
- self.last_pos = None
- self.Refresh()
+ self.dp.clear()
+
- def on_size(self, e):
- # ensure that the whole canvas is visible
- window_width, window_height = self.GetSize()
- client_width, client_height = self.GetClientSize()
+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)
- decorations_width = window_width - client_width
- decorations_height = window_height - client_height + 40
+ self.simulator_panel = SimulatorPanel(self,
+ stitch_plan=stitch_plan,
+ target_duration=target_duration,
+ stitches_per_second=stitches_per_second)
+ self.Bind(wx.EVT_CLOSE, self.on_close)
- setsize_window_width = self.width * self.scale + decorations_width + self.margin * 2
- setsize_window_height = (self.height) * self.scale + decorations_height + self.margin * 2
+ def quit(self):
+ self.Close()
- # set minimum width (force space for control buttons)
- if setsize_window_width < self.min_width:
- setsize_window_width = self.min_width
+ def on_close(self, event):
+ self.simulator_panel.stop()
- self.SetSize(( setsize_window_width, setsize_window_height))
+ if self.on_close_hook:
+ self.on_close_hook()
- # center the simulation on screen if not called by params
- # else center vertically
- if self.x_position == None:
- self.Centre()
- else:
- display_rect = self.get_current_screen_rect()
- self.SetPosition((self.x_position, display_rect[3] / 2 - setsize_window_height / 2))
+ self.Destroy()
- e.Skip()
+ def go(self):
+ self.simulator_panel.go()
- def on_paint(self, e):
- dc = wx.PaintDC(self.panel)
- dc.Blit(0, 0, self.buffer.GetWidth(), self.buffer.GetHeight(), self.dc, 0, 0)
+ def stop(self):
+ self.simulator_panel.stop()
- if self.last_pos:
- dc.DrawLine(self.last_pos[0] - 10, self.last_pos[1], self.last_pos[0] + 10, self.last_pos[1])
- dc.DrawLine(self.last_pos[0], self.last_pos[1] - 10, self.last_pos[0], self.last_pos[1] + 10)
+ def load(self, stitch_plan):
+ self.simulator_panel.load(stitch_plan)
- def draw_one_frame(self):
- for i in xrange(self.stitches_per_frame):
- try:
- ((x1, y1), (x2, y2)), color = self.segments[self.current_stitch]
+ def clear(self):
+ self.simulator_panel.clear()
- if self.mirror:
- y1 = self.height - y1
- y2 = self.height - y2
+def show_simulator(stitch_plan):
+ app = wx.App()
+ current_screen = wx.Display.GetFromPoint(wx.GetMousePosition())
+ display = wx.Display(current_screen)
+ screen_rect = display.GetClientArea()
- x1 = x1 * self.scale + self.margin
- y1 = y1 * self.scale + self.margin
- x2 = x2 * self.scale + self.margin
- y2 = y2 * self.scale + self.margin
+ simulator_pos = (screen_rect[0], screen_rect[1])
- self.canvas.SetPen(color)
- self.canvas.DrawLines(((x1, y1), (x2, y2)))
- self.Refresh()
+ # subtract 1 because otherwise the window becomes maximized on Linux
+ width = screen_rect[2] - 1
+ height = screen_rect[3] - 1
- self.current_stitch += 1
- self.last_pos = (x2, y2)
+ frame = EmbroiderySimulator(None, -1, _("Embroidery Simulation"), pos=simulator_pos, size=(width, height), stitch_plan=stitch_plan)
+ app.SetTopWindow(frame)
+ frame.Show()
+ app.MainLoop()
- self.set_stitch_counter(self.current_stitch + 1)
- except IndexError:
- self.timer.Stop()
+if __name__ == "__main__":
+ stitch_plan = stitch_plan_from_file(sys.argv[1])
+ show_simulator(stitch_plan)
diff --git a/lib/stitch_plan/__init__.py b/lib/stitch_plan/__init__.py
index 791a5f20..2aaa0ab9 100644
--- a/lib/stitch_plan/__init__.py
+++ b/lib/stitch_plan/__init__.py
@@ -1,2 +1,3 @@
-from stitch_plan import patches_to_stitch_plan, StitchPlan, ColorBlock
+from .stitch_plan import patches_to_stitch_plan, StitchPlan, ColorBlock
from .stitch import Stitch
+from .read_file import stitch_plan_from_file
diff --git a/lib/stitch_plan/read_file.py b/lib/stitch_plan/read_file.py
new file mode 100644
index 00000000..ff5a68ac
--- /dev/null
+++ b/lib/stitch_plan/read_file.py
@@ -0,0 +1,21 @@
+import pyembroidery
+from .stitch_plan import StitchPlan
+
+from ..svg import PIXELS_PER_MM
+
+
+def stitch_plan_from_file(embroidery_file):
+ """Read a machine embroidery file in any supported format and return a stitch plan."""
+ pattern = pyembroidery.read(embroidery_file)
+
+ stitch_plan = StitchPlan()
+ color_block = None
+
+ for raw_stitches, thread in pattern.get_as_colorblocks():
+ color_block = stitch_plan.new_color_block(thread)
+ for x, y, command in raw_stitches:
+ color_block.add_stitch(x * PIXELS_PER_MM / 10.0, y * PIXELS_PER_MM / 10.0,
+ jump=(command == pyembroidery.JUMP),
+ trim=(command == pyembroidery.TRIM))
+
+ return stitch_plan
diff --git a/lib/stitch_plan/stitch_plan.py b/lib/stitch_plan/stitch_plan.py
index c713b42e..808fa626 100644
--- a/lib/stitch_plan/stitch_plan.py
+++ b/lib/stitch_plan/stitch_plan.py
@@ -36,6 +36,12 @@ def patches_to_stitch_plan(patches, collapse_len=3.0 * PIXELS_PER_MM):
# make a new block of our color
color_block = stitch_plan.new_color_block(color=patch.color)
+ # always start a color with a JUMP to the first stitch position
+ color_block.add_stitch(patch.stitches[0], jump=True)
+ else:
+ if len(color_block) and (patch.stitches[0] - color_block.stitches[-1]).length() > collapse_len:
+ color_block.add_stitch(patch.stitches[0], jump=True)
+
color_block.add_stitches(patch.stitches, no_ties=patch.stitch_as_is)
if patch.trim_after:
diff --git a/messages.po b/messages.po
index f483d294..7756f1cf 100644
--- a/messages.po
+++ b/messages.po
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
-"POT-Creation-Date: 2018-09-12 20:31-0400\n"
+"POT-Creation-Date: 2018-09-12 20:51-0400\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -505,7 +505,7 @@ msgstr ""
msgid "Install"
msgstr ""
-#: lib/extensions/install.py:40 lib/extensions/params.py:409
+#: lib/extensions/install.py:40 lib/extensions/params.py:408
msgid "Cancel"
msgstr ""
@@ -594,45 +594,45 @@ msgstr ""
msgid "Inkscape objects"
msgstr ""
-#: lib/extensions/params.py:352
+#: lib/extensions/params.py:351
msgid ""
"Click to force this parameter to be saved when you click \"Apply and "
"Quit\""
msgstr ""
-#: lib/extensions/params.py:360
+#: lib/extensions/params.py:359
msgid "This parameter will be saved when you click \"Apply and Quit\""
msgstr ""
-#: lib/extensions/params.py:375
+#: lib/extensions/params.py:374
msgid "Embroidery Params"
msgstr ""
-#: lib/extensions/params.py:392
+#: lib/extensions/params.py:391
msgid "Presets"
msgstr ""
-#: lib/extensions/params.py:397
+#: lib/extensions/params.py:396
msgid "Load"
msgstr ""
-#: lib/extensions/params.py:400
+#: lib/extensions/params.py:399
msgid "Add"
msgstr ""
-#: lib/extensions/params.py:403
+#: lib/extensions/params.py:402
msgid "Overwrite"
msgstr ""
-#: lib/extensions/params.py:406
+#: lib/extensions/params.py:405
msgid "Delete"
msgstr ""
-#: lib/extensions/params.py:413
+#: lib/extensions/params.py:412
msgid "Use Last Settings"
msgstr ""
-#: lib/extensions/params.py:416
+#: lib/extensions/params.py:415
msgid "Apply and Quit"
msgstr ""
@@ -640,25 +640,25 @@ msgstr ""
msgid "Preview"
msgstr ""
-#: lib/extensions/params.py:487
+#: lib/extensions/params.py:484
msgid "Internal Error"
msgstr ""
-#: lib/extensions/params.py:540
+#: lib/extensions/params.py:537
msgid "Please enter or select a preset name first."
msgstr ""
-#: lib/extensions/params.py:540 lib/extensions/params.py:546
-#: lib/extensions/params.py:574
+#: lib/extensions/params.py:537 lib/extensions/params.py:543
+#: lib/extensions/params.py:571
msgid "Preset"
msgstr ""
-#: lib/extensions/params.py:546
+#: lib/extensions/params.py:543
#, python-format
msgid "Preset \"%s\" not found."
msgstr ""
-#: lib/extensions/params.py:574
+#: lib/extensions/params.py:571
#, python-format
msgid ""
"Preset \"%s\" already exists. Please use another name or press "
@@ -687,10 +687,6 @@ msgstr ""
msgid "Ink/Stitch Print"
msgstr ""
-#: lib/extensions/simulate.py:29
-msgid "Embroidery Simulation"
-msgstr ""
-
#: lib/extensions/zip.py:49
msgid "No embroidery file formats selected."
msgstr ""
@@ -704,57 +700,55 @@ msgstr ""
#. low-level file error. %(error)s is (hopefully?) translated by
#. the user's system automatically.
-#: lib/output.py:94
+#: lib/output.py:102
#, python-format
msgid "Error writing to %(path)s: %(error)s"
msgstr ""
-#: lib/simulator.py:40
-msgid "Speed up"
+#. command label at bottom of simulator window
+#: lib/simulator.py:12
+msgid "STITCH"
msgstr ""
-#: lib/simulator.py:40
-msgid "Press + or arrow up to speed up"
+#: lib/simulator.py:12
+msgid "JUMP"
msgstr ""
-#: lib/simulator.py:41
-msgid "Slow down"
+#: lib/simulator.py:12
+msgid "TRIM"
msgstr ""
-#: lib/simulator.py:41
-msgid "Press - or arrow down to slow down"
+#: lib/simulator.py:12
+msgid "STOP"
msgstr ""
-#: lib/simulator.py:42
-msgid "Pause"
+#: lib/simulator.py:12
+msgid "COLOR CHANGE"
msgstr ""
-#: lib/simulator.py:42
-msgid "Press P to pause the animation"
+#: lib/simulator.py:49 lib/simulator.py:208 lib/simulator.py:215
+msgid "Pause"
msgstr ""
-#: lib/simulator.py:43
+#: lib/simulator.py:51
msgid "Restart"
msgstr ""
-#: lib/simulator.py:43
-msgid "Press R to restart the animation"
-msgstr ""
-
-#: lib/simulator.py:44
+#: lib/simulator.py:53
msgid "Quit"
msgstr ""
-#: lib/simulator.py:44
-msgid "Press Q to close the simulation window"
+#: lib/simulator.py:168
+#, python-format
+msgid "Speed: %d stitches/sec"
msgstr ""
-#: lib/simulator.py:169
-msgid "Stitch # "
+#: lib/simulator.py:211
+msgid "Start"
msgstr ""
-#: lib/simulator.py:172
-msgid "Stitch #"
+#: lib/simulator.py:633
+msgid "Embroidery Simulation"
msgstr ""
#: lib/stitches/auto_fill.py:167