summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/elements/fill.py5
-rw-r--r--lib/extensions/output.py7
-rw-r--r--lib/extensions/params.py20
-rw-r--r--lib/extensions/simulate.py9
-rw-r--r--lib/simulator.py151
-rw-r--r--lib/stitches/auto_fill.py113
-rw-r--r--lib/utils/geometry.py15
7 files changed, 219 insertions, 101 deletions
diff --git a/lib/elements/fill.py b/lib/elements/fill.py
index 8018b2b4..394f523e 100644
--- a/lib/elements/fill.py
+++ b/lib/elements/fill.py
@@ -105,9 +105,12 @@ class Fill(EmbroideryElement):
last_pt = pt
else:
last_pt = pt
- if point_ary:
+ if len(point_ary) > 2:
poly_ary.append(point_ary)
+ if not poly_ary:
+ self.fatal(_("shape %s is so small that it cannot be filled with stitches. Please make it bigger or delete it.") % self.node.get('id'))
+
# shapely's idea of "holes" are to subtract everything in the second set
# from the first. So let's at least make sure the "first" thing is the
# biggest path.
diff --git a/lib/extensions/output.py b/lib/extensions/output.py
index 1dc8d19d..f3bb0a80 100644
--- a/lib/extensions/output.py
+++ b/lib/extensions/output.py
@@ -36,10 +36,15 @@ class Output(InkstitchExtension):
write_embroidery_file(temp_file.name, stitch_plan, self.document.getroot())
+ if sys.platform == "win32":
+ import msvcrt
+ msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
+
# inkscape will read the file contents from stdout and copy
# to the destination file that the user chose
- with open(temp_file.name) as output_file:
+ with open(temp_file.name, "rb") as output_file:
sys.stdout.write(output_file.read())
+ sys.stdout.flush()
# clean up the temp file
os.remove(temp_file.name)
diff --git a/lib/extensions/params.py b/lib/extensions/params.py
index 1b8f2589..c464e044 100644
--- a/lib/extensions/params.py
+++ b/lib/extensions/params.py
@@ -424,18 +424,22 @@ class SettingsFrame(wx.Frame):
self.simulate_window.stop()
self.simulate_window.load(stitch_plan=stitch_plan)
else:
- my_rect = self.GetScreenRect()
- simulator_pos = my_rect.GetTopRight()
+ params_rect = self.GetScreenRect()
+ simulator_pos = params_rect.GetTopRight()
simulator_pos.x += 5
- screen_rect = wx.Display(0).ClientArea
- max_width = screen_rect.GetWidth() - my_rect.GetWidth()
+ current_screen = wx.Display.GetFromPoint(wx.GetMousePosition())
+ display = wx.Display(current_screen)
+ screen_rect = display.GetClientArea()
+
+ max_width = screen_rect.GetWidth() - params_rect.GetWidth()
max_height = screen_rect.GetHeight()
try:
self.simulate_window = EmbroiderySimulator(None, -1, _("Preview"),
simulator_pos,
size=(300, 300),
+ x_position=simulator_pos.x,
stitch_plan=stitch_plan,
on_close=self.simulate_window_closed,
target_duration=5,
@@ -764,6 +768,14 @@ class Params(InkstitchExtension):
try:
app = wx.App()
frame = SettingsFrame(tabs_factory=self.create_tabs, on_cancel=self.cancel)
+
+ # position left, center
+ current_screen = wx.Display.GetFromPoint(wx.GetMousePosition())
+ display = wx.Display(current_screen)
+ display_size = display.GetClientArea()
+ frame_size = frame.GetSize()
+ frame.SetPosition((display_size[0], display_size[3] / 2 - frame_size[1] / 2))
+
frame.Show()
app.MainLoop()
diff --git a/lib/extensions/simulate.py b/lib/extensions/simulate.py
index 0c372d4d..38f86156 100644
--- a/lib/extensions/simulate.py
+++ b/lib/extensions/simulate.py
@@ -17,11 +17,16 @@ class Simulate(InkstitchExtension):
def effect(self):
if not self.get_elements():
return
-
patches = self.elements_to_patches(self.elements)
stitch_plan = patches_to_stitch_plan(patches)
app = wx.App()
- frame = EmbroiderySimulator(None, -1, _("Embroidery Simulation"), wx.DefaultPosition, size=(1000, 1000), stitch_plan=stitch_plan)
+ 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)
diff --git a/lib/simulator.py b/lib/simulator.py
index 2d2d3e08..5620f65b 100644
--- a/lib/simulator.py
+++ b/lib/simulator.py
@@ -10,38 +10,44 @@ from .svg import PIXELS_PER_MM, color_block_to_point_lists
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 = 10
+ self.margin = 30
- 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())
+ 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.SetFocus()
+
+ 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')],
- [_("Slow down"),_('Press - or arrow down to slow down')],
- [_("Pause"),_("Press P to pause the animation")],
- [_("Restart"),_("Press R to restart the animation")],
- [_("Quit"),_("Press Q to close the simulation window")])
+ [_("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].Bind(wx.EVT_BUTTON, self.on_key_down)
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)
@@ -59,15 +65,51 @@ class EmbroiderySimulator(wx.Frame):
self.clear()
+ self.set_stitch_counter(1)
+
+ 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_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_DOWN, self.animation_slow_down),
+ (wx.ACCEL_NORMAL, ord('r'), self.animation_restart),
+ (wx.ACCEL_NORMAL, ord('p'), self.animation_pause),
+ (wx.ACCEL_NORMAL, ord('q'), self.animation_quit)]
+
+ accel_entries = []
+
+ for shortcut_key in shortcut_keys:
+ eventId = wx.NewId()
+ accel_entries.append((shortcut_key[0], shortcut_key[1], eventId))
+ self.Bind(wx.EVT_MENU, shortcut_key[2], id=eventId)
+
+ accel_table = wx.AcceleratorTable(accel_entries)
+ self.SetAcceleratorTable(accel_table)
+
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)
- self.panel.Bind(wx.EVT_KEY_DOWN, self.on_key_down)
+
+ self.panel.SetFocus()
self.timer = None
self.last_pos = None
- self.Bind(wx.EVT_CLOSE, self.on_close)
+ 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 load(self, stitch_plan=None):
if stitch_plan:
@@ -87,42 +129,51 @@ class EmbroiderySimulator(wx.Frame):
self.frame_period *= 2
self.stitches_per_frame *= 2
- def on_key_down(self, event):
- if hasattr(event, 'GetKeyCode'):
- keycode = event.GetKeyCode()
+ def animation_speed_up(self, event):
+ if self.frame_period == 1:
+ self.stitches_per_frame *= 2
else:
- keycode = event.GetEventObject().GetLabelText()
- self.panel.SetFocus()
-
- if keycode == ord("+") or keycode == ord("=") or keycode == wx.WXK_UP or keycode == "Speed 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 or keycode == "Slow down":
- if self.stitches_per_frame == 1:
- self.frame_period *= 2
- else:
- self.stitches_per_frame /= 2
- elif keycode == ord("Q") or keycode == "Quit":
- self.Close()
- elif keycode == ord("P") or keycode == "Pause":
- if self.timer.IsRunning():
- self.timer.Stop()
- else:
- self.timer.Start(self.frame_period)
- elif keycode == ord("R") or keycode == "Restart":
- self.stop()
- self.clear()
- self.go()
+ self.frame_period = self.frame_period / 2
+ self.animation_update_timer()
+ 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_restart(self, event):
+ self.stop()
+ self.clear()
+ self.go()
+
+ def animation_pause(self, event):
+ if self.timer.IsRunning():
+ self.timer.Stop()
+ else:
+ self.timer.Start(self.frame_period)
+
+ def animation_quit(self, event):
+ self.Close()
+
+ 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 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')
+
def _strip_quotes(self, string):
if string.startswith('"') and string.endswith('"'):
string = string[1:-1]
@@ -194,7 +245,7 @@ class EmbroiderySimulator(wx.Frame):
self.width = width
self.height = height
- self.scale = min(float(self.max_width) / width, float(self.max_height - 60) / height)
+ self.scale = min(float(self.max_width - self.margin * 2) / width, float(self.max_height - self.margin * 2 - 40) / height)
# make room for decorations and the margin
self.scale *= 0.95
@@ -241,11 +292,20 @@ class EmbroiderySimulator(wx.Frame):
setsize_window_width = self.width * self.scale + decorations_width + self.margin * 2
setsize_window_height = (self.height) * self.scale + decorations_height + self.margin * 2
- if setsize_window_width < 600:
- setsize_window_width = 600
+ # set minimum width (force space for control buttons)
+ if setsize_window_width < self.min_width:
+ setsize_window_width = self.min_width
self.SetSize(( setsize_window_width, setsize_window_height))
+ # 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))
+
e.Skip()
def on_paint(self, e):
@@ -276,5 +336,8 @@ class EmbroiderySimulator(wx.Frame):
self.current_stitch += 1
self.last_pos = (x2, y2)
+
+ self.set_stitch_counter(self.current_stitch + 1)
+
except IndexError:
self.timer.Stop()
diff --git a/lib/stitches/auto_fill.py b/lib/stitches/auto_fill.py
index 6326ced2..097ab1d9 100644
--- a/lib/stitches/auto_fill.py
+++ b/lib/stitches/auto_fill.py
@@ -6,9 +6,10 @@ from itertools import groupby, izip
from collections import deque
from .fill import intersect_region_with_grating, row_num, stitch_row
+from .running_stitch import running_stitch
from ..i18n import _
from ..svg import PIXELS_PER_MM
-from ..utils.geometry import Point as InkstitchPoint
+from ..utils.geometry import Point as InkstitchPoint, cut
class MaxQueueLengthExceeded(Exception):
@@ -437,58 +438,86 @@ def collapse_sequential_outline_edges(graph, path):
return new_path
-def outline_distance(outline, p1, p2):
- # how far around the outline (and in what direction) do I need to go
- # to get from p1 to p2?
-
- p1_projection = outline.project(shapely.geometry.Point(p1))
- p2_projection = outline.project(shapely.geometry.Point(p2))
-
- distance = p2_projection - p1_projection
-
- if abs(distance) > outline.length / 2.0:
- # if we'd have to go more than halfway around, it's faster to go
- # the other way
- if distance < 0:
- return distance + outline.length
- elif distance > 0:
- return distance - outline.length
- else:
- # this ought not happen, but just for completeness, return 0 if
- # p1 and p0 are the same point
- return 0
- else:
- return distance
+def connect_points(shape, start, end, running_stitch_length, row_spacing):
+ """Create stitches to get from one point on an outline of the shape to another.
+ An outline is essentially a loop (a path of points that ends where it starts).
+ Given point A and B on that loop, we want to take the shortest path from one
+ to the other. Due to the way our path-finding algorithm above works, it may
+ have had to take the long way around the shape to get from A to B, but we'd
+ rather ignore that and just get there the short way.
+ """
-def connect_points(shape, start, end, running_stitch_length):
+ # We may be on the outer boundary or on on of the hole boundaries.
outline_index = which_outline(shape, start)
outline = shape.boundary[outline_index]
- pos = outline.project(shapely.geometry.Point(start))
- distance = outline_distance(outline, start, end)
- num_stitches = abs(int(distance / running_stitch_length))
-
- direction = math.copysign(1.0, distance)
- one_stitch = running_stitch_length * direction
-
- stitches = [InkstitchPoint(*outline.interpolate(pos).coords[0])]
-
- for i in xrange(num_stitches):
- pos = (pos + one_stitch) % outline.length
-
- stitches.append(InkstitchPoint(*outline.interpolate(pos).coords[0]))
+ # First, figure out the start and end position along the outline. The
+ # projection gives us the distance travelled down the outline to get to
+ # that point.
+ start = shapely.geometry.Point(start)
+ start_projection = outline.project(start)
+ end = shapely.geometry.Point(end)
+ end_projection = outline.project(end)
+
+ # If the points are pretty close, just jump there. There's a slight
+ # risk that we're going to sew outside the shape here. The way to
+ # avoid that is to use running_stitch() even for these really short
+ # connections, but that would be really slow for all of the
+ # connections from one row to the next.
+ #
+ # This seems to do a good job of avoiding going outside the shape in
+ # most cases. 1.4 is chosen as approximately the length of the
+ # stitch connecting two rows if the side of the shape is at a 45
+ # degree angle to the rows of stitches (sqrt(2)).
+ if abs(end_projection - start_projection) < row_spacing * 1.4:
+ return [InkstitchPoint(end.x, end.y)]
+
+ # The outline path has a "natural" starting point. Think of this as
+ # 0 or 12 on an analog clock.
+
+ # Cut the outline into two paths at the starting point. The first
+ # section will go from 12 o'clock to the starting point. The second
+ # section will go from the starting point all the way around and end
+ # up at 12 again.
+ result = cut(outline, start_projection)
+
+ # result will be None if our starting point happens to already be at
+ # 12 o'clock.
+ if result is not None and result[1] is not None:
+ before, after = result
+
+ # Make a new outline, starting from the starting point. This is
+ # like rotating the clock so that now our starting point is
+ # at 12 o'clock.
+ outline = shapely.geometry.LineString(list(after.coords) + list(before.coords))
+
+ # Now figure out where our ending point is on the newly-rotated clock.
+ end_projection = outline.project(end)
+
+ # Cut the new path at the ending point. before and after now represent
+ # two ways to get from the starting point to the ending point. One
+ # will most likely be longer than the other.
+ before, after = cut(outline, end_projection)
+
+ if before.length <= after.length:
+ points = list(before.coords)
+ else:
+ # after goes from the ending point to the starting point, so reverse
+ # it to get from start to end.
+ points = list(reversed(after.coords))
- end = InkstitchPoint(*end)
- if (end - stitches[-1]).length() > 0.1 * PIXELS_PER_MM:
- stitches.append(end)
+ # Now do running stitch along the path we've found. running_stitch() will
+ # avoid cutting sharp corners.
+ path = [InkstitchPoint(*p) for p in points]
+ return running_stitch(path, running_stitch_length)
- return stitches
def trim_end(path):
while path and path[-1].is_outline():
path.pop()
+
def path_to_stitches(graph, path, shape, angle, row_spacing, max_stitch_length, running_stitch_length, staggers):
path = collapse_sequential_outline_edges(graph, path)
@@ -498,6 +527,6 @@ def path_to_stitches(graph, path, shape, angle, row_spacing, max_stitch_length,
if edge.is_segment():
stitch_row(stitches, edge[0], edge[1], angle, row_spacing, max_stitch_length, staggers)
else:
- stitches.extend(connect_points(shape, edge[0], edge[1], running_stitch_length))
+ stitches.extend(connect_points(shape, edge[0], edge[1], running_stitch_length, row_spacing))
return stitches
diff --git a/lib/utils/geometry.py b/lib/utils/geometry.py
index d0cb96cf..bfdcd3c0 100644
--- a/lib/utils/geometry.py
+++ b/lib/utils/geometry.py
@@ -9,21 +9,22 @@ def cut(line, distance):
"""
if distance <= 0.0 or distance >= line.length:
return [LineString(line), None]
- coords = list(line.coords)
- for i, p in enumerate(coords):
- # TODO: I think this doesn't work if the path doubles back on itself
- pd = line.project(ShapelyPoint(p))
- if pd == distance:
+ coords = list(ShapelyPoint(p) for p in line.coords)
+ traveled = 0
+ last_point = coords[0]
+ for i, p in enumerate(coords[1:], 1):
+ traveled += p.distance(last_point)
+ last_point = p
+ if traveled == distance:
return [
LineString(coords[:i+1]),
LineString(coords[i:])]
- if pd > distance:
+ if traveled > distance:
cp = line.interpolate(distance)
return [
LineString(coords[:i] + [(cp.x, cp.y)]),
LineString([(cp.x, cp.y)] + coords[i:])]
-
def cut_path(points, length):
"""Return a subsection of at the start of the path that is length units long.