diff options
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/elements/fill.py | 5 | ||||
| -rw-r--r-- | lib/extensions/output.py | 7 | ||||
| -rw-r--r-- | lib/extensions/params.py | 20 | ||||
| -rw-r--r-- | lib/extensions/simulate.py | 9 | ||||
| -rw-r--r-- | lib/simulator.py | 151 | ||||
| -rw-r--r-- | lib/stitches/auto_fill.py | 113 | ||||
| -rw-r--r-- | lib/utils/geometry.py | 15 |
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. |
