2017-10-03 12:10:08 +00:00
|
|
|
import sys
|
|
|
|
import os
|
|
|
|
import numpy
|
|
|
|
import wx
|
|
|
|
import inkex
|
2017-12-30 21:05:21 +00:00
|
|
|
import simplestyle
|
2018-01-01 20:26:18 +00:00
|
|
|
import colorsys
|
2017-10-03 12:10:08 +00:00
|
|
|
|
2018-02-05 03:38:24 +00:00
|
|
|
import inkstitch
|
Fix simulate (#42)
* Simulate now works regardless of the output format you chose when you ran Embroider.
* Simulate (and the preview in Params) now respects TRIMs.
* Inkscape restart required (embroider.inx changed).
This one kind of grew in the telling. #37 was a theoretically simple bug, but in reality, the code necessary to fix it was the straw that broke the camel's back, and I had to do a fair bit of (much needed) code reorganization. Mostly the reorganization was just under the hood, but there was one user-facing change around the Embroider extension's settings window.
Way back in the day, the only way to control things like the stitch length or satin density was through global options specified in the extension settings. We've long since moved to per-object params, but for backward compatibility, ink/stitch defaulted to the command-line arguments.
That means that it was possible to get different stitch results from the same SVG file if you changed the extension's settings. For that reason, I never touched mine. I didn't intend for my users to use those extension-level settings at all, and I've planned to remove those settings for awhile now.
At this point, the extension settings just getting in the way of implementing more features, so I'm getting rid of them and moving the defaults into the parameters system. I've still left things like the output format and the collapse length (although I'm considering moving that one too).
2018-01-28 21:10:37 +00:00
|
|
|
from inkstitch import PIXELS_PER_MM
|
2018-02-05 03:38:24 +00:00
|
|
|
from embroider import _, patches_to_stitches, get_elements, elements_to_patches
|
|
|
|
|
2017-12-28 20:55:43 +00:00
|
|
|
|
2017-10-03 12:10:08 +00:00
|
|
|
class EmbroiderySimulator(wx.Frame):
|
|
|
|
def __init__(self, *args, **kwargs):
|
2017-12-28 20:55:43 +00:00
|
|
|
stitch_file = kwargs.pop('stitch_file', None)
|
|
|
|
patches = kwargs.pop('patches', None)
|
2017-12-30 21:05:21 +00:00
|
|
|
self.on_close_hook = kwargs.pop('on_close', None)
|
2017-10-24 02:46:52 +00:00
|
|
|
self.frame_period = kwargs.pop('frame_period', 80)
|
2017-10-03 12:10:08 +00:00
|
|
|
self.stitches_per_frame = kwargs.pop('stitches_per_frame', 1)
|
2018-01-01 03:24:18 +00:00
|
|
|
self.target_duration = kwargs.pop('target_duration', None)
|
2018-01-06 20:48:36 +00:00
|
|
|
|
2018-01-30 01:09:34 +00:00
|
|
|
self.margin = 10
|
|
|
|
|
2018-01-06 20:48:36 +00:00
|
|
|
screen_rect = wx.Display(0).ClientArea
|
|
|
|
self.max_width = kwargs.pop('max_width', screen_rect.GetWidth())
|
|
|
|
self.max_height = kwargs.pop('max_height', screen_rect.GetHeight())
|
|
|
|
self.scale = 1
|
2017-10-03 12:10:08 +00:00
|
|
|
|
|
|
|
wx.Frame.__init__(self, *args, **kwargs)
|
2017-12-30 21:05:21 +00:00
|
|
|
|
2017-10-03 12:10:08 +00:00
|
|
|
self.panel = wx.Panel(self, wx.ID_ANY)
|
|
|
|
self.panel.SetFocus()
|
|
|
|
|
2017-12-28 20:55:43 +00:00
|
|
|
self.load(stitch_file, patches)
|
2017-10-03 12:10:08 +00:00
|
|
|
|
2018-01-01 03:24:18 +00:00
|
|
|
if self.target_duration:
|
|
|
|
self.adjust_speed(self.target_duration)
|
|
|
|
|
2018-01-30 01:09:34 +00:00
|
|
|
self.buffer = wx.Bitmap(self.width * self.scale + self.margin * 2, self.height * self.scale + self.margin * 2)
|
2017-10-03 12:10:08 +00:00
|
|
|
self.dc = wx.MemoryDC()
|
|
|
|
self.dc.SelectObject(self.buffer)
|
|
|
|
self.canvas = wx.GraphicsContext.Create(self.dc)
|
|
|
|
|
|
|
|
self.clear()
|
|
|
|
|
|
|
|
self.Bind(wx.EVT_SIZE, self.on_size)
|
|
|
|
self.panel.Bind(wx.EVT_PAINT, self.on_paint)
|
|
|
|
self.panel.Bind(wx.EVT_KEY_DOWN, self.on_key_down)
|
|
|
|
|
2017-12-30 21:05:21 +00:00
|
|
|
self.timer = None
|
|
|
|
|
2017-10-03 12:10:08 +00:00
|
|
|
self.last_pos = None
|
|
|
|
|
2017-12-30 21:05:21 +00:00
|
|
|
self.Bind(wx.EVT_CLOSE, self.on_close)
|
|
|
|
|
2017-12-28 20:55:43 +00:00
|
|
|
def load(self, stitch_file=None, patches=None):
|
|
|
|
if stitch_file:
|
2018-01-01 03:05:05 +00:00
|
|
|
self.mirror = True
|
2017-12-28 20:55:43 +00:00
|
|
|
self.segments = self._parse_stitch_file(stitch_file)
|
|
|
|
elif patches:
|
2018-01-01 03:05:05 +00:00
|
|
|
self.mirror = False
|
2017-12-30 21:05:21 +00:00
|
|
|
self.segments = self._patches_to_segments(patches)
|
2017-12-28 20:55:43 +00:00
|
|
|
else:
|
2017-12-30 21:05:21 +00:00
|
|
|
return
|
2017-12-28 20:55:43 +00:00
|
|
|
|
2018-01-01 03:38:15 +00:00
|
|
|
self.trim_margins()
|
2018-01-06 20:48:36 +00:00
|
|
|
self.calculate_dimensions()
|
2017-12-28 20:55:43 +00:00
|
|
|
|
2018-01-01 03:24:18 +00:00
|
|
|
def adjust_speed(self, duration):
|
|
|
|
self.frame_period = 1000 * float(duration) / len(self.segments)
|
|
|
|
self.stitches_per_frame = 1
|
|
|
|
|
|
|
|
while self.frame_period < 1.0:
|
|
|
|
self.frame_period *= 2
|
|
|
|
self.stitches_per_frame *= 2
|
|
|
|
|
2017-10-03 12:10:08 +00:00
|
|
|
def on_key_down(self, event):
|
|
|
|
keycode = event.GetKeyCode()
|
|
|
|
|
|
|
|
if keycode == ord("+") or keycode == ord("=") or keycode == wx.WXK_UP:
|
|
|
|
if self.frame_period == 1:
|
|
|
|
self.stitches_per_frame *= 2
|
|
|
|
else:
|
|
|
|
self.frame_period = self.frame_period / 2
|
|
|
|
elif keycode == ord("-") or keycode == ord("_") or keycode == wx.WXK_DOWN:
|
|
|
|
if self.stitches_per_frame == 1:
|
|
|
|
self.frame_period *= 2
|
|
|
|
else:
|
|
|
|
self.stitches_per_frame /= 2
|
|
|
|
elif keycode == ord("Q"):
|
|
|
|
self.Close()
|
|
|
|
elif keycode == ord('P'):
|
|
|
|
if self.timer.IsRunning():
|
|
|
|
self.timer.Stop()
|
|
|
|
else:
|
|
|
|
self.timer.Start(self.frame_period)
|
2018-01-01 20:00:58 +00:00
|
|
|
elif keycode == ord("R"):
|
|
|
|
self.stop()
|
|
|
|
self.clear()
|
|
|
|
self.go()
|
2017-10-03 12:10:08 +00:00
|
|
|
|
|
|
|
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 _strip_quotes(self, string):
|
|
|
|
if string.startswith('"') and string.endswith('"'):
|
|
|
|
string = string[1:-1]
|
|
|
|
|
|
|
|
return string
|
|
|
|
|
2018-01-01 20:26:18 +00:00
|
|
|
def color_to_pen(self, color):
|
|
|
|
# python colorsys module uses floats from 0 to 1.0
|
|
|
|
color = [value / 255.0 for value in color]
|
|
|
|
|
|
|
|
hls = list(colorsys.rgb_to_hls(*color))
|
|
|
|
|
|
|
|
# Our background is white. If the color is too close to white, then
|
|
|
|
# it won't be visible. Capping lightness should make colors visible
|
|
|
|
# without changing them too much.
|
|
|
|
hls[1] = min(hls[1], 0.85)
|
|
|
|
|
|
|
|
color = colorsys.hls_to_rgb(*hls)
|
|
|
|
|
|
|
|
# convert back to values in the range of 0-255
|
|
|
|
color = [value * 255 for value in color]
|
|
|
|
|
|
|
|
return wx.Pen(color)
|
|
|
|
|
2017-12-30 21:05:21 +00:00
|
|
|
def _patches_to_segments(self, patches):
|
2017-12-28 20:55:43 +00:00
|
|
|
stitches = patches_to_stitches(patches)
|
|
|
|
|
|
|
|
segments = []
|
|
|
|
|
|
|
|
last_pos = None
|
|
|
|
last_color = None
|
2017-12-30 21:05:21 +00:00
|
|
|
pen = None
|
Fix simulate (#42)
* Simulate now works regardless of the output format you chose when you ran Embroider.
* Simulate (and the preview in Params) now respects TRIMs.
* Inkscape restart required (embroider.inx changed).
This one kind of grew in the telling. #37 was a theoretically simple bug, but in reality, the code necessary to fix it was the straw that broke the camel's back, and I had to do a fair bit of (much needed) code reorganization. Mostly the reorganization was just under the hood, but there was one user-facing change around the Embroider extension's settings window.
Way back in the day, the only way to control things like the stitch length or satin density was through global options specified in the extension settings. We've long since moved to per-object params, but for backward compatibility, ink/stitch defaulted to the command-line arguments.
That means that it was possible to get different stitch results from the same SVG file if you changed the extension's settings. For that reason, I never touched mine. I didn't intend for my users to use those extension-level settings at all, and I've planned to remove those settings for awhile now.
At this point, the extension settings just getting in the way of implementing more features, so I'm getting rid of them and moving the defaults into the parameters system. I've still left things like the output format and the collapse length (although I'm considering moving that one too).
2018-01-28 21:10:37 +00:00
|
|
|
trimming = False
|
2017-12-28 20:55:43 +00:00
|
|
|
|
|
|
|
for stitch in stitches:
|
Fix simulate (#42)
* Simulate now works regardless of the output format you chose when you ran Embroider.
* Simulate (and the preview in Params) now respects TRIMs.
* Inkscape restart required (embroider.inx changed).
This one kind of grew in the telling. #37 was a theoretically simple bug, but in reality, the code necessary to fix it was the straw that broke the camel's back, and I had to do a fair bit of (much needed) code reorganization. Mostly the reorganization was just under the hood, but there was one user-facing change around the Embroider extension's settings window.
Way back in the day, the only way to control things like the stitch length or satin density was through global options specified in the extension settings. We've long since moved to per-object params, but for backward compatibility, ink/stitch defaulted to the command-line arguments.
That means that it was possible to get different stitch results from the same SVG file if you changed the extension's settings. For that reason, I never touched mine. I didn't intend for my users to use those extension-level settings at all, and I've planned to remove those settings for awhile now.
At this point, the extension settings just getting in the way of implementing more features, so I'm getting rid of them and moving the defaults into the parameters system. I've still left things like the output format and the collapse length (although I'm considering moving that one too).
2018-01-28 21:10:37 +00:00
|
|
|
if stitch.trim:
|
|
|
|
trimming = True
|
|
|
|
last_pos = None
|
|
|
|
continue
|
|
|
|
|
|
|
|
if trimming:
|
|
|
|
if stitch.jump:
|
|
|
|
continue
|
|
|
|
else:
|
|
|
|
trimming = False
|
|
|
|
|
2017-12-30 21:05:21 +00:00
|
|
|
pos = (stitch.x, stitch.y)
|
|
|
|
|
2017-12-28 20:55:43 +00:00
|
|
|
if stitch.color == last_color:
|
Fix simulate (#42)
* Simulate now works regardless of the output format you chose when you ran Embroider.
* Simulate (and the preview in Params) now respects TRIMs.
* Inkscape restart required (embroider.inx changed).
This one kind of grew in the telling. #37 was a theoretically simple bug, but in reality, the code necessary to fix it was the straw that broke the camel's back, and I had to do a fair bit of (much needed) code reorganization. Mostly the reorganization was just under the hood, but there was one user-facing change around the Embroider extension's settings window.
Way back in the day, the only way to control things like the stitch length or satin density was through global options specified in the extension settings. We've long since moved to per-object params, but for backward compatibility, ink/stitch defaulted to the command-line arguments.
That means that it was possible to get different stitch results from the same SVG file if you changed the extension's settings. For that reason, I never touched mine. I didn't intend for my users to use those extension-level settings at all, and I've planned to remove those settings for awhile now.
At this point, the extension settings just getting in the way of implementing more features, so I'm getting rid of them and moving the defaults into the parameters system. I've still left things like the output format and the collapse length (although I'm considering moving that one too).
2018-01-28 21:10:37 +00:00
|
|
|
if last_pos:
|
|
|
|
segments.append(((last_pos, pos), pen))
|
2017-12-30 21:05:21 +00:00
|
|
|
else:
|
2018-01-01 20:26:18 +00:00
|
|
|
pen = self.color_to_pen(simplestyle.parseColor(stitch.color))
|
2017-12-28 20:55:43 +00:00
|
|
|
|
|
|
|
last_pos = pos
|
|
|
|
last_color = stitch.color
|
|
|
|
|
|
|
|
return segments
|
|
|
|
|
2018-01-01 03:38:15 +00:00
|
|
|
def all_coordinates(self):
|
|
|
|
for segment in self.segments:
|
|
|
|
start, end = segment[0]
|
|
|
|
|
|
|
|
yield start
|
|
|
|
yield end
|
|
|
|
|
|
|
|
def trim_margins(self):
|
|
|
|
"""remove any unnecessary whitespace around the design"""
|
|
|
|
|
|
|
|
min_x = sys.maxint
|
|
|
|
min_y = sys.maxint
|
|
|
|
|
|
|
|
for x, y in self.all_coordinates():
|
|
|
|
min_x = min(min_x, x)
|
|
|
|
min_y = min(min_y, y)
|
|
|
|
|
|
|
|
new_segments = []
|
|
|
|
|
|
|
|
for segment in self.segments:
|
|
|
|
(start, end), color = segment
|
|
|
|
|
|
|
|
new_segment = (
|
|
|
|
(
|
|
|
|
(start[0] - min_x, start[1] - min_y),
|
|
|
|
(end[0] - min_x, end[1] - min_y),
|
|
|
|
),
|
|
|
|
color
|
|
|
|
)
|
|
|
|
|
|
|
|
new_segments.append(new_segment)
|
|
|
|
|
|
|
|
self.segments = new_segments
|
|
|
|
|
2018-01-06 20:48:36 +00:00
|
|
|
def calculate_dimensions(self):
|
2018-01-30 01:09:34 +00:00
|
|
|
# 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
|
2017-10-03 12:10:08 +00:00
|
|
|
|
2018-01-01 03:38:15 +00:00
|
|
|
for x, y in self.all_coordinates():
|
|
|
|
width = max(width, x)
|
|
|
|
height = max(height, y)
|
2017-10-03 12:10:08 +00:00
|
|
|
|
2018-01-06 20:48:36 +00:00
|
|
|
self.width = width
|
|
|
|
self.height = height
|
|
|
|
self.scale = min(float(self.max_width) / width, float(self.max_height) / height)
|
|
|
|
|
2018-01-30 01:09:34 +00:00
|
|
|
# make room for decorations and the margin
|
2018-01-06 20:48:36 +00:00
|
|
|
self.scale *= 0.95
|
2017-10-03 12:10:08 +00:00
|
|
|
|
|
|
|
def go(self):
|
2017-12-28 20:55:43 +00:00
|
|
|
self.clear()
|
|
|
|
|
2017-10-03 12:10:08 +00:00
|
|
|
self.current_stitch = 0
|
2018-01-01 20:00:58 +00:00
|
|
|
|
|
|
|
if not self.timer:
|
|
|
|
self.timer = wx.PyTimer(self.draw_one_frame)
|
|
|
|
|
2017-10-03 12:10:08 +00:00
|
|
|
self.timer.Start(self.frame_period)
|
|
|
|
|
2017-12-30 21:05:21 +00:00
|
|
|
def on_close(self, event):
|
|
|
|
self.stop()
|
|
|
|
|
|
|
|
if self.on_close_hook:
|
|
|
|
self.on_close_hook()
|
|
|
|
|
|
|
|
self.Destroy()
|
|
|
|
|
2017-12-28 20:55:43 +00:00
|
|
|
def stop(self):
|
2017-12-30 21:05:21 +00:00
|
|
|
if self.timer:
|
|
|
|
self.timer.Stop()
|
2017-12-28 20:55:43 +00:00
|
|
|
|
2017-10-03 12:10:08 +00:00
|
|
|
def clear(self):
|
|
|
|
self.dc.SetBackground(wx.Brush('white'))
|
|
|
|
self.dc.Clear()
|
2017-12-31 01:54:35 +00:00
|
|
|
self.last_pos = None
|
|
|
|
self.Refresh()
|
2017-10-03 12:10:08 +00:00
|
|
|
|
|
|
|
def on_size(self, e):
|
|
|
|
# ensure that the whole canvas is visible
|
|
|
|
window_width, window_height = self.GetSize()
|
|
|
|
client_width, client_height = self.GetClientSize()
|
|
|
|
|
|
|
|
decorations_width = window_width - client_width
|
|
|
|
decorations_height = window_height - client_height
|
|
|
|
|
2018-01-30 01:09:34 +00:00
|
|
|
self.SetSize((self.width * self.scale + decorations_width + self.margin * 2,
|
|
|
|
self.height * self.scale + decorations_height + self.margin * 2))
|
2017-10-03 12:10:08 +00:00
|
|
|
|
|
|
|
e.Skip()
|
|
|
|
|
|
|
|
def on_paint(self, e):
|
|
|
|
dc = wx.PaintDC(self.panel)
|
2018-02-20 02:43:39 +00:00
|
|
|
dc.Blit(0, 0, self.buffer.GetWidth(), self.buffer.GetHeight(), self.dc, 0, 0)
|
2017-10-03 12:10:08 +00:00
|
|
|
|
|
|
|
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)
|
|
|
|
|
2017-12-28 20:39:56 +00:00
|
|
|
def draw_one_frame(self):
|
2017-10-03 12:10:08 +00:00
|
|
|
for i in xrange(self.stitches_per_frame):
|
|
|
|
try:
|
2017-12-28 20:55:43 +00:00
|
|
|
((x1, y1), (x2, y2)), color = self.segments[self.current_stitch]
|
2018-01-01 03:05:05 +00:00
|
|
|
|
|
|
|
if self.mirror:
|
|
|
|
y1 = self.height - y1
|
|
|
|
y2 = self.height - y2
|
2017-10-03 12:10:08 +00:00
|
|
|
|
2018-01-30 01:09:34 +00:00
|
|
|
x1 = x1 * self.scale + self.margin
|
|
|
|
y1 = y1 * self.scale + self.margin
|
|
|
|
x2 = x2 * self.scale + self.margin
|
|
|
|
y2 = y2 * self.scale + self.margin
|
2018-01-06 02:58:22 +00:00
|
|
|
|
2017-10-03 12:10:08 +00:00
|
|
|
self.canvas.SetPen(color)
|
|
|
|
self.canvas.DrawLines(((x1, y1), (x2, y2)))
|
|
|
|
self.Refresh()
|
|
|
|
|
|
|
|
self.current_stitch += 1
|
|
|
|
self.last_pos = (x2, y2)
|
|
|
|
except IndexError:
|
|
|
|
self.timer.Stop()
|
|
|
|
|
|
|
|
class SimulateEffect(inkex.Effect):
|
|
|
|
def __init__(self):
|
|
|
|
inkex.Effect.__init__(self)
|
|
|
|
self.OptionParser.add_option("-P", "--path",
|
|
|
|
action="store", type="string",
|
|
|
|
dest="path", default=".",
|
|
|
|
help="Directory in which to store output file")
|
|
|
|
|
|
|
|
def effect(self):
|
Fix simulate (#42)
* Simulate now works regardless of the output format you chose when you ran Embroider.
* Simulate (and the preview in Params) now respects TRIMs.
* Inkscape restart required (embroider.inx changed).
This one kind of grew in the telling. #37 was a theoretically simple bug, but in reality, the code necessary to fix it was the straw that broke the camel's back, and I had to do a fair bit of (much needed) code reorganization. Mostly the reorganization was just under the hood, but there was one user-facing change around the Embroider extension's settings window.
Way back in the day, the only way to control things like the stitch length or satin density was through global options specified in the extension settings. We've long since moved to per-object params, but for backward compatibility, ink/stitch defaulted to the command-line arguments.
That means that it was possible to get different stitch results from the same SVG file if you changed the extension's settings. For that reason, I never touched mine. I didn't intend for my users to use those extension-level settings at all, and I've planned to remove those settings for awhile now.
At this point, the extension settings just getting in the way of implementing more features, so I'm getting rid of them and moving the defaults into the parameters system. I've still left things like the output format and the collapse length (although I'm considering moving that one too).
2018-01-28 21:10:37 +00:00
|
|
|
patches = elements_to_patches(get_elements(self))
|
2017-10-03 12:10:08 +00:00
|
|
|
app = wx.App()
|
2018-02-05 03:38:24 +00:00
|
|
|
frame = EmbroiderySimulator(None, -1, _("Embroidery Simulation"), wx.DefaultPosition, size=(1000, 1000), patches=patches)
|
2017-10-03 12:10:08 +00:00
|
|
|
app.SetTopWindow(frame)
|
|
|
|
frame.Show()
|
|
|
|
wx.CallAfter(frame.go)
|
|
|
|
app.MainLoop()
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
effect = SimulateEffect()
|
|
|
|
effect.affect()
|
|
|
|
sys.exit(0)
|