move extension classes into inkstitch/extensions and add inkstitch.py

pull/163/head
Lex Neva 2018-04-28 21:26:53 -04:00
rodzic a42726679a
commit 32695e195a
10 zmienionych plików z 1673 dodań i 13 usunięć

36
inkstitch.py 100644
Wyświetl plik

@ -0,0 +1,36 @@
import sys
import traceback
from argparse import ArgumentParser
from inkstitch.utils import save_stderr, restore_stderr
from inkstitch import extensions
def get_extension():
parser = ArgumentParser()
parser.add_argument("--extension")
args, extras = parser.parse_known_args()
return args.extension
extension_name = get_extension()
extension_class = getattr(extensions, extension_name.capitalize())
extension = extension_class()
exception = None
save_stderr()
try:
extension.affect()
except (SystemExit, KeyboardInterrupt):
raise
except Exception:
exception = traceback.format_exc()
finally:
restore_stderr()
if exception:
print >> sys.stderr, exception
sys.exit(1)
else:
sys.exit(0)

Wyświetl plik

@ -0,0 +1,5 @@
from embroider import Embroider
from palettes import Palettes
from params import Params
from print import Print
from simulate import Simulate

Wyświetl plik

@ -3,9 +3,9 @@ import re
import json
from copy import deepcopy
from collections import MutableMapping
from .elements import AutoFill, Fill, Stroke, SatinColumn, Polyline, EmbroideryElement
from . import SVG_POLYLINE_TAG, SVG_GROUP_TAG, SVG_DEFS_TAG, INKSCAPE_GROUPMODE, EMBROIDERABLE_TAGS, PIXELS_PER_MM
from .utils import cache
from ..elements import AutoFill, Fill, Stroke, SatinColumn, Polyline, EmbroideryElement
from .. import SVG_POLYLINE_TAG, SVG_GROUP_TAG, SVG_DEFS_TAG, INKSCAPE_GROUPMODE, EMBROIDERABLE_TAGS, PIXELS_PER_MM
from ..utils import cache
SVG_METADATA_TAG = inkex.addNS("metadata", "svg")

Wyświetl plik

@ -0,0 +1,86 @@
import sys
import traceback
import os
import inkex
from .. import _, PIXELS_PER_MM, write_embroidery_file
from .base import InkstitchExtension
from ..stitch_plan import patches_to_stitch_plan
from ..svg import render_stitch_plan
class Embroider(InkstitchExtension):
def __init__(self, *args, **kwargs):
InkstitchExtension.__init__(self)
self.OptionParser.add_option("-c", "--collapse_len_mm",
action="store", type="float",
dest="collapse_length_mm", default=3.0,
help="max collapse length (mm)")
self.OptionParser.add_option("--hide_layers",
action="store", type="choice",
choices=["true", "false"],
dest="hide_layers", default="true",
help="Hide all other layers when the embroidery layer is generated")
self.OptionParser.add_option("-O", "--output_format",
action="store", type="string",
dest="output_format", default="csv",
help="Output file extenstion (default: csv)")
self.OptionParser.add_option("-P", "--path",
action="store", type="string",
dest="path", default=".",
help="Directory in which to store output file")
self.OptionParser.add_option("-F", "--output-file",
action="store", type="string",
dest="output_file",
help="Output filename.")
self.OptionParser.add_option("-b", "--max-backups",
action="store", type="int",
dest="max_backups", default=5,
help="Max number of backups of output files to keep.")
self.OptionParser.usage += _("\n\nSeeing a 'no such option' message? Please restart Inkscape to fix.")
def get_output_path(self):
if self.options.output_file:
output_path = os.path.join(self.options.path, self.options.output_file)
else:
svg_filename = self.document.getroot().get(inkex.addNS('docname', 'sodipodi'), "embroidery.svg")
csv_filename = svg_filename.replace('.svg', '.%s' % self.options.output_format)
output_path = os.path.join(self.options.path, csv_filename)
def add_suffix(path, suffix):
if suffix > 0:
path = "%s.%s" % (path, suffix)
return path
def move_if_exists(path, suffix=0):
source = add_suffix(path, suffix)
if suffix >= self.options.max_backups:
return
dest = add_suffix(path, suffix + 1)
if os.path.exists(source):
move_if_exists(path, suffix + 1)
if os.path.exists(dest):
os.remove(dest)
os.rename(source, dest)
move_if_exists(output_path)
return output_path
def effect(self):
if not self.get_elements():
return
if self.options.hide_layers:
self.hide_all_layers()
patches = self.elements_to_patches(self.elements)
stitch_plan = patches_to_stitch_plan(patches, self.options.collapse_length_mm * PIXELS_PER_MM)
write_embroidery_file(self.get_output_path(), stitch_plan, self.document.getroot())
render_stitch_plan(self.document.getroot(), stitch_plan)

Wyświetl plik

@ -0,0 +1,111 @@
import sys
import traceback
import os
from os.path import realpath, dirname
from glob import glob
from threading import Thread
import socket
import errno
import time
import logging
import wx
import inkex
from ..utils import guess_inkscape_config_path
class InstallPalettesFrame(wx.Frame):
def __init__(self, *args, **kwargs):
wx.Frame.__init__(self, *args, **kwargs)
default_path = os.path.join(guess_inkscape_config_path(), "palettes")
panel = wx.Panel(self)
sizer = wx.BoxSizer(wx.VERTICAL)
text = wx.StaticText(panel, label=_("Directory in which to install palettes:"))
font = wx.Font(12, wx.DEFAULT, wx.NORMAL, wx.NORMAL)
text.SetFont(font)
sizer.Add(text, proportion=0, flag=wx.ALL|wx.EXPAND, border=10)
path_sizer = wx.BoxSizer(wx.HORIZONTAL)
self.path_input = wx.TextCtrl(panel, wx.ID_ANY, value=default_path)
path_sizer.Add(self.path_input, proportion=3, flag=wx.RIGHT|wx.EXPAND, border=20)
chooser_button = wx.Button(panel, wx.ID_OPEN, _('Choose another directory...'))
path_sizer.Add(chooser_button, proportion=1, flag=wx.EXPAND)
sizer.Add(path_sizer, proportion=0, flag=wx.ALL|wx.EXPAND, border=10)
buttons_sizer = wx.BoxSizer(wx.HORIZONTAL)
install_button = wx.Button(panel, wx.ID_ANY, _("Install"))
install_button.SetBitmap(wx.ArtProvider.GetBitmap(wx.ART_TICK_MARK))
buttons_sizer.Add(install_button, proportion=0, flag=wx.ALIGN_RIGHT|wx.ALL, border=5)
cancel_button = wx.Button(panel, wx.ID_CANCEL, _("Cancel"))
buttons_sizer.Add(cancel_button, proportion=0, flag=wx.ALIGN_RIGHT|wx.ALL, border=5)
sizer.Add(buttons_sizer, proportion=0, flag=wx.ALIGN_RIGHT)
outer_sizer = wx.BoxSizer(wx.HORIZONTAL)
outer_sizer.Add(sizer, proportion=0, flag=wx.ALIGN_CENTER_VERTICAL)
panel.SetSizer(outer_sizer)
panel.Layout()
chooser_button.Bind(wx.EVT_BUTTON, self.chooser_button_clicked)
cancel_button.Bind(wx.EVT_BUTTON, self.cancel_button_clicked)
install_button.Bind(wx.EVT_BUTTON, self.install_button_clicked)
def cancel_button_clicked(self, event):
self.Destroy()
def chooser_button_clicked(self, event):
dialog = wx.DirDialog(self, _("Choose Inkscape palettes directory"))
if dialog.ShowModal() != wx.ID_CANCEL:
self.path_input.SetValue(dialog.GetPath())
def install_button_clicked(self, event):
try:
self.install_palettes()
except Exception, e:
wx.MessageDialog(self,
_('Thread palette installation failed') + ': \n' + traceback.format_exc(),
_('Installation Failed'),
wx.OK).ShowModal()
else:
wx.MessageDialog(self,
_('Thread palette files have been installed. Please restart Inkscape to load the new palettes.'),
_('Installation Completed'),
wx.OK).ShowModal()
self.Destroy()
def install_palettes(self):
path = self.path_input.GetValue()
palettes_dir = self.get_bundled_palettes_dir()
self.copy_files(glob(os.path.join(palettes_dir, "*")), path)
def get_bundled_palettes_dir(self):
if getattr(sys, 'frozen', None) is not None:
return realpath(os.path.join(sys._MEIPASS, '..', 'palettes'))
else:
return os.path.join(dirname(realpath(__file__)), 'palettes')
if (sys.platform == "win32"):
# If we try to just use shutil.copy it says the operation requires elevation.
def copy_files(self, files, dest):
import winutils
winutils.copy(files, dest)
else:
def copy_files(self, files, dest):
import shutil
if not os.path.exists(dest):
os.makedirs(dest)
for palette_file in files:
shutil.copy(palette_file, dest)
class Palettes(inkex.Effect):
def effect(self):
app = wx.App()
installer_frame = InstallPalettesFrame(None, title=_("Ink/Stitch Thread Palette Installer"), size=(450, 200))
installer_frame.Show()
app.MainLoop()

Wyświetl plik

@ -0,0 +1,754 @@
import os
import sys
import json
import traceback
import time
from threading import Thread, Event
from copy import copy
import wx
from wx.lib.scrolledpanel import ScrolledPanel
from collections import defaultdict
from functools import partial
from itertools import groupby
from .. import _
from .base import InkstitchExtension
from ..stitch_plan import patches_to_stitch_plan
from ..elements import EmbroideryElement, Fill, AutoFill, Stroke, SatinColumn
from ..utils import save_stderr, restore_stderr
from ..simulator import EmbroiderySimulator
def presets_path():
try:
import appdirs
config_path = appdirs.user_config_dir('inkstitch')
except ImportError:
config_path = os.path.expanduser('~/.inkstitch')
if not os.path.exists(config_path):
os.makedirs(config_path)
return os.path.join(config_path, 'presets.json')
def load_presets():
try:
with open(presets_path(), 'r') as presets:
presets = json.load(presets)
return presets
except:
return {}
def save_presets(presets):
with open(presets_path(), 'w') as presets_file:
json.dump(presets, presets_file)
def load_preset(name):
return load_presets().get(name)
def save_preset(name, data):
presets = load_presets()
presets[name] = data
save_presets(presets)
def delete_preset(name):
presets = load_presets()
presets.pop(name, None)
save_presets(presets)
def confirm_dialog(parent, question, caption = 'ink/stitch'):
dlg = wx.MessageDialog(parent, question, caption, wx.YES_NO | wx.ICON_QUESTION)
result = dlg.ShowModal() == wx.ID_YES
dlg.Destroy()
return result
def info_dialog(parent, message, caption = 'ink/stitch'):
dlg = wx.MessageDialog(parent, message, caption, wx.OK | wx.ICON_INFORMATION)
dlg.ShowModal()
dlg.Destroy()
class ParamsTab(ScrolledPanel):
def __init__(self, *args, **kwargs):
self.params = kwargs.pop('params', [])
self.name = kwargs.pop('name', None)
self.nodes = kwargs.pop('nodes')
kwargs["style"] = wx.TAB_TRAVERSAL
ScrolledPanel.__init__(self, *args, **kwargs)
self.SetupScrolling()
self.changed_inputs = set()
self.dependent_tabs = []
self.parent_tab = None
self.param_inputs = {}
self.paired_tab = None
self.disable_notify_pair = False
toggles = [param for param in self.params if param.type == 'toggle']
if toggles:
self.toggle = toggles[0]
self.params.remove(self.toggle)
self.toggle_checkbox = wx.CheckBox(self, label=self.toggle.description)
value = any(self.toggle.values)
if self.toggle.inverse:
value = not value
self.toggle_checkbox.SetValue(value)
self.toggle_checkbox.Bind(wx.EVT_CHECKBOX, self.update_toggle_state)
self.toggle_checkbox.Bind(wx.EVT_CHECKBOX, self.changed)
self.param_inputs[self.toggle.name] = self.toggle_checkbox
else:
self.toggle = None
self.settings_grid = wx.FlexGridSizer(rows=0, cols=3, hgap=10, vgap=10)
self.settings_grid.AddGrowableCol(0, 1)
self.settings_grid.SetFlexibleDirection(wx.HORIZONTAL)
self.__set_properties()
self.__do_layout()
if self.toggle:
self.update_toggle_state()
# end wxGlade
def pair(self, tab):
# print self.name, "paired with", tab.name
self.paired_tab = tab
self.update_description()
def add_dependent_tab(self, tab):
self.dependent_tabs.append(tab)
self.update_description()
def set_parent_tab(self, tab):
self.parent_tab = tab
def is_dependent_tab(self):
return self.parent_tab is not None
def enabled(self):
if self.toggle_checkbox:
return self.toggle_checkbox.IsChecked()
else:
return True
def update_toggle_state(self, event=None, notify_pair=True):
enable = self.enabled()
# print self.name, "update_toggle_state", enable
for child in self.settings_grid.GetChildren():
widget = child.GetWindow()
if widget:
child.GetWindow().Enable(enable)
if notify_pair and self.paired_tab:
self.paired_tab.pair_changed(enable)
for tab in self.dependent_tabs:
tab.dependent_enable(enable)
if event:
event.Skip()
def pair_changed(self, value):
# print self.name, "pair_changed", value
new_value = not value
if self.enabled() != new_value:
self.set_toggle_state(not value)
self.update_toggle_state(notify_pair=False)
if self.on_change_hook:
self.on_change_hook(self)
def dependent_enable(self, enable):
if enable:
self.toggle_checkbox.Enable()
else:
self.set_toggle_state(False)
self.toggle_checkbox.Disable()
self.update_toggle_state()
if self.on_change_hook:
self.on_change_hook(self)
def set_toggle_state(self, value):
if self.toggle_checkbox:
self.toggle_checkbox.SetValue(value)
self.changed_inputs.add(self.toggle_checkbox)
def get_values(self):
values = {}
if self.toggle:
checked = self.enabled()
if self.toggle_checkbox in self.changed_inputs and not self.toggle.inverse:
values[self.toggle.name] = checked
if not checked:
# Ignore params on this tab if the toggle is unchecked,
# because they're grayed out anyway.
return values
for name, input in self.param_inputs.iteritems():
if input in self.changed_inputs and input != self.toggle_checkbox:
values[name] = input.GetValue()
return values
def apply(self):
values = self.get_values()
for node in self.nodes:
# print >> sys.stderr, "apply: ", self.name, node.id, values
for name, value in values.iteritems():
node.set_param(name, value)
def on_change(self, callable):
self.on_change_hook = callable
def changed(self, event):
self.changed_inputs.add(event.GetEventObject())
event.Skip()
if self.on_change_hook:
self.on_change_hook(self)
def load_preset(self, preset):
preset_data = preset.get(self.name, {})
for name, value in preset_data.iteritems():
if name in self.param_inputs:
self.param_inputs[name].SetValue(value)
self.changed_inputs.add(self.param_inputs[name])
self.update_toggle_state()
def save_preset(self, storage):
preset = storage[self.name] = {}
for name, input in self.param_inputs.iteritems():
preset[name] = input.GetValue()
def update_description(self):
if len(self.nodes) == 1:
description = _("These settings will be applied to 1 object.")
else:
description = _("These settings will be applied to %d objects.") % len(self.nodes)
if any(len(param.values) > 1 for param in self.params):
description += "\n" + _("Some settings had different values across objects. Select a value from the dropdown or enter a new one.")
if self.dependent_tabs:
if len(self.dependent_tabs) == 1:
description += "\n" + _("Disabling this tab will disable the following %d tabs.") % len(self.dependent_tabs)
else:
description += "\n" + _("Disabling this tab will disable the following tab.")
if self.paired_tab:
description += "\n" + _("Enabling this tab will disable %s and vice-versa.") % self.paired_tab.name
self.description_text = description
def resized(self, event):
if not hasattr(self, 'rewrap_timer'):
self.rewrap_timer = wx.Timer()
self.rewrap_timer.Bind(wx.EVT_TIMER, self.rewrap)
# If we try to rewrap every time we get EVT_SIZE then a resize is
# extremely slow.
self.rewrap_timer.Start(50, oneShot=True)
event.Skip()
def rewrap(self, event=None):
self.description.SetLabel(self.description_text)
self.description.Wrap(self.GetSize().x - 20)
self.description_container.Layout()
if event:
event.Skip()
def __set_properties(self):
# begin wxGlade: SatinPane.__set_properties
# end wxGlade
pass
def __do_layout(self):
# just to add space around the settings
box = wx.BoxSizer(wx.VERTICAL)
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)
self.description_container = box
self.Bind(wx.EVT_SIZE, self.resized)
sizer.Add(self.description, proportion=0, flag=wx.EXPAND|wx.ALL, border=5)
box.Add(sizer, proportion=0, flag=wx.ALL, border=5)
if self.toggle:
box.Add(self.toggle_checkbox, proportion=0, flag=wx.BOTTOM, border=10)
for param in self.params:
description = wx.StaticText(self, label=param.description)
description.SetToolTip(param.tooltip)
self.settings_grid.Add(description, proportion=1, flag=wx.EXPAND|wx.RIGHT, border=40)
if param.type == 'boolean':
if len(param.values) > 1:
input = wx.CheckBox(self, style=wx.CHK_3STATE)
input.Set3StateValue(wx.CHK_UNDETERMINED)
else:
input = wx.CheckBox(self)
if param.values:
input.SetValue(param.values[0])
input.Bind(wx.EVT_CHECKBOX, self.changed)
elif len(param.values) > 1:
input = wx.ComboBox(self, wx.ID_ANY, choices=sorted(param.values), style=wx.CB_DROPDOWN)
input.Bind(wx.EVT_COMBOBOX, self.changed)
input.Bind(wx.EVT_TEXT, self.changed)
else:
value = param.values[0] if param.values else ""
input = wx.TextCtrl(self, wx.ID_ANY, value=str(value))
input.Bind(wx.EVT_TEXT, self.changed)
self.param_inputs[param.name] = input
self.settings_grid.Add(input, proportion=1, flag=wx.ALIGN_CENTER_VERTICAL)
self.settings_grid.Add(wx.StaticText(self, label=param.unit or ""), proportion=1, flag=wx.ALIGN_CENTER_VERTICAL)
box.Add(self.settings_grid, proportion=1, flag=wx.ALL, border=10)
self.SetSizer(box)
self.Layout()
# end of class SatinPane
class SettingsFrame(wx.Frame):
def __init__(self, *args, **kwargs):
# begin wxGlade: MyFrame.__init__
self.tabs_factory = kwargs.pop('tabs_factory', [])
self.cancel_hook = kwargs.pop('on_cancel', None)
wx.Frame.__init__(self, None, wx.ID_ANY,
_("Embroidery Params")
)
self.notebook = wx.Notebook(self, wx.ID_ANY)
self.tabs = self.tabs_factory(self.notebook)
for tab in self.tabs:
tab.on_change(self.update_simulator)
self.simulate_window = None
self.simulate_thread = None
self.simulate_refresh_needed = Event()
wx.CallLater(1000, self.update_simulator)
self.presets_box = wx.StaticBox(self, wx.ID_ANY, label=_("Presets"))
self.preset_chooser = wx.ComboBox(self, wx.ID_ANY)
self.update_preset_list()
self.load_preset_button = wx.Button(self, wx.ID_ANY, _("Load"))
self.load_preset_button.Bind(wx.EVT_BUTTON, self.load_preset)
self.add_preset_button = wx.Button(self, wx.ID_ANY, _("Add"))
self.add_preset_button.Bind(wx.EVT_BUTTON, self.add_preset)
self.overwrite_preset_button = wx.Button(self, wx.ID_ANY, _("Overwrite"))
self.overwrite_preset_button.Bind(wx.EVT_BUTTON, self.overwrite_preset)
self.delete_preset_button = wx.Button(self, wx.ID_ANY, _("Delete"))
self.delete_preset_button.Bind(wx.EVT_BUTTON, self.delete_preset)
self.cancel_button = wx.Button(self, wx.ID_ANY, _("Cancel"))
self.cancel_button.Bind(wx.EVT_BUTTON, self.cancel)
self.Bind(wx.EVT_CLOSE, self.cancel)
self.use_last_button = wx.Button(self, wx.ID_ANY, _("Use Last Settings"))
self.use_last_button.Bind(wx.EVT_BUTTON, self.use_last)
self.apply_button = wx.Button(self, wx.ID_ANY, _("Apply and Quit"))
self.apply_button.Bind(wx.EVT_BUTTON, self.apply)
self.__set_properties()
self.__do_layout()
# end wxGlade
def update_simulator(self, tab=None):
if self.simulate_window:
self.simulate_window.stop()
self.simulate_window.clear()
if not self.simulate_thread or not self.simulate_thread.is_alive():
self.simulate_thread = Thread(target=self.simulate_worker)
self.simulate_thread.daemon = True
self.simulate_thread.start()
self.simulate_refresh_needed.set()
def simulate_worker(self):
while True:
self.simulate_refresh_needed.wait()
self.simulate_refresh_needed.clear()
self.update_patches()
def update_patches(self):
patches = self.generate_patches()
if patches and not self.simulate_refresh_needed.is_set():
wx.CallAfter(self.refresh_simulator, patches)
def refresh_simulator(self, patches):
stitch_plan = patches_to_stitch_plan(patches)
if self.simulate_window:
self.simulate_window.stop()
self.simulate_window.load(stitch_plan=stitch_plan)
else:
my_rect = self.GetRect()
simulator_pos = my_rect.GetTopRight()
simulator_pos.x += 5
screen_rect = wx.Display(0).ClientArea
max_width = screen_rect.GetWidth() - my_rect.GetWidth()
max_height = screen_rect.GetHeight()
try:
self.simulate_window = EmbroiderySimulator(None, -1, _("Preview"),
simulator_pos,
size=(300, 300),
stitch_plan=stitch_plan,
on_close=self.simulate_window_closed,
target_duration=5,
max_width=max_width,
max_height=max_height)
except:
error = traceback.format_exc()
try:
# a window may have been created, so we need to destroy it
# or the app will never exit
wx.Window.FindWindowByName("Preview").Destroy()
except:
pass
info_dialog(self, error, _("Internal Error"))
self.simulate_window.Show()
wx.CallLater(10, self.Raise)
wx.CallAfter(self.simulate_window.go)
def simulate_window_closed(self):
self.simulate_window = None
def generate_patches(self):
patches = []
nodes = []
for tab in self.tabs:
tab.apply()
if tab.enabled() and not tab.is_dependent_tab():
nodes.extend(tab.nodes)
# sort nodes into the proper stacking order
nodes.sort(key=lambda node: node.order)
try:
for node in nodes:
if self.simulate_refresh_needed.is_set():
# cancel; params were updated and we need to start over
return []
# Making a copy of the embroidery element is an easy
# way to drop the cache in the @cache decorators used
# for many params in embroider.py.
patches.extend(copy(node).embroider(None))
except SystemExit:
raise
except:
# Ignore errors. This can be things like incorrect paths for
# satins or division by zero caused by incorrect param values.
pass
return patches
def update_preset_list(self):
preset_names = load_presets().keys()
preset_names = [preset for preset in preset_names if preset != "__LAST__"]
self.preset_chooser.SetItems(sorted(preset_names))
def get_preset_name(self):
preset_name = self.preset_chooser.GetValue().strip()
if preset_name:
return preset_name
else:
info_dialog(self, _("Please enter or select a preset name first."), caption=_('Preset'))
return
def check_and_load_preset(self, preset_name):
preset = load_preset(preset_name)
if not preset:
info_dialog(self, _('Preset "%s" not found.') % preset_name, caption=_('Preset'))
return preset
def get_preset_data(self):
preset = {}
current_tab = self.tabs[self.notebook.GetSelection()]
while current_tab.parent_tab:
current_tab = current_tab.parent_tab
tabs = [current_tab]
if current_tab.paired_tab:
tabs.append(current_tab.paired_tab)
tabs.extend(current_tab.paired_tab.dependent_tabs)
tabs.extend(current_tab.dependent_tabs)
for tab in tabs:
tab.save_preset(preset)
return preset
def add_preset(self, event, overwrite=False):
preset_name = self.get_preset_name()
if not preset_name:
return
if not overwrite and load_preset(preset_name):
info_dialog(self, _('Preset "%s" already exists. Please use another name or press "Overwrite"') % preset_name, caption=_('Preset'))
save_preset(preset_name, self.get_preset_data())
self.update_preset_list()
event.Skip()
def overwrite_preset(self, event):
self.add_preset(event, overwrite=True)
def _load_preset(self, preset_name):
preset = self.check_and_load_preset(preset_name)
if not preset:
return
for tab in self.tabs:
tab.load_preset(preset)
def load_preset(self, event):
preset_name = self.get_preset_name()
if not preset_name:
return
self._load_preset(preset_name)
event.Skip()
def delete_preset(self, event):
preset_name = self.get_preset_name()
if not preset_name:
return
preset = self.check_and_load_preset(preset_name)
if not preset:
return
delete_preset(preset_name)
self.update_preset_list()
self.preset_chooser.SetValue("")
event.Skip()
def _apply(self):
for tab in self.tabs:
tab.apply()
def apply(self, event):
self._apply()
save_preset("__LAST__", self.get_preset_data())
self.close()
def use_last(self, event):
self._load_preset("__LAST__")
self.apply(event)
def close(self):
if self.simulate_window:
self.simulate_window.stop()
self.simulate_window.Close()
self.Destroy()
def cancel(self, event):
if self.cancel_hook:
self.cancel_hook()
self.close()
def __set_properties(self):
# begin wxGlade: MyFrame.__set_properties
self.notebook.SetMinSize((800, 600))
self.preset_chooser.SetSelection(-1)
# end wxGlade
def __do_layout(self):
# begin wxGlade: MyFrame.__do_layout
sizer_1 = wx.BoxSizer(wx.VERTICAL)
#self.sizer_3_staticbox.Lower()
sizer_2 = wx.StaticBoxSizer(self.presets_box, wx.HORIZONTAL)
sizer_3 = wx.BoxSizer(wx.HORIZONTAL)
for tab in self.tabs:
self.notebook.AddPage(tab, tab.name)
sizer_1.Add(self.notebook, 1, wx.EXPAND|wx.LEFT|wx.TOP|wx.RIGHT, 10)
sizer_2.Add(self.preset_chooser, 1, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5)
sizer_2.Add(self.load_preset_button, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5)
sizer_2.Add(self.add_preset_button, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5)
sizer_2.Add(self.overwrite_preset_button, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5)
sizer_2.Add(self.delete_preset_button, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5)
sizer_1.Add(sizer_2, 0, flag=wx.EXPAND|wx.ALL, border=10)
sizer_3.Add(self.cancel_button, 0, wx.ALIGN_RIGHT|wx.RIGHT, 5)
sizer_3.Add(self.use_last_button, 0, wx.ALIGN_RIGHT|wx.RIGHT|wx.BOTTOM, 5)
sizer_3.Add(self.apply_button, 0, wx.ALIGN_RIGHT|wx.RIGHT|wx.BOTTOM, 5)
sizer_1.Add(sizer_3, 0, wx.ALIGN_RIGHT, 0)
self.SetSizer(sizer_1)
sizer_1.Fit(self)
self.Layout()
# end wxGlade
class EmbroiderParams(InkstitchExtension):
def __init__(self, *args, **kwargs):
self.cancelled = False
InkstitchExtension.__init__(self, *args, **kwargs)
def embroidery_classes(self, node):
element = EmbroideryElement(node)
classes = []
if element.get_style("fill"):
classes.append(AutoFill)
classes.append(Fill)
if element.get_style("stroke"):
classes.append(Stroke)
if element.get_style("stroke-dasharray") is None:
classes.append(SatinColumn)
return classes
def get_nodes_by_class(self):
nodes = self.get_nodes()
nodes_by_class = defaultdict(list)
for z, node in enumerate(nodes):
for cls in self.embroidery_classes(node):
element = cls(node)
element.order = z
nodes_by_class[cls].append(element)
return sorted(nodes_by_class.items(), key=lambda (cls, nodes): cls.__name__)
def get_values(self, param, nodes):
getter = 'get_param'
if param.type in ('toggle', 'boolean'):
getter = 'get_boolean_param'
else:
getter = 'get_param'
values = filter(lambda item: item is not None,
(getattr(node, getter)(param.name, str(param.default)) for node in nodes))
return values
def group_params(self, params):
def by_group_and_sort_index(param):
return param.group, param.sort_index
def by_group(param):
return param.group
return groupby(sorted(params, key=by_group_and_sort_index), by_group)
def create_tabs(self, parent):
tabs = []
for cls, nodes in self.get_nodes_by_class():
params = cls.get_params()
for param in params:
param.values = list(set(self.get_values(param, nodes)))
parent_tab = None
new_tabs = []
for group, params in self.group_params(params):
tab = ParamsTab(parent, id=wx.ID_ANY, name=group or cls.element_name, params=list(params), nodes=nodes)
new_tabs.append(tab)
if group is None:
parent_tab = tab
for tab in new_tabs:
if tab != parent_tab:
parent_tab.add_dependent_tab(tab)
tab.set_parent_tab(parent_tab)
tabs.extend(new_tabs)
for tab in tabs:
if tab.toggle and tab.toggle.inverse:
for other_tab in tabs:
if other_tab != tab and other_tab.toggle.name == tab.toggle.name:
tab.pair(other_tab)
other_tab.pair(tab)
def tab_sort_key(tab):
parent = tab.parent_tab or tab
sort_key = (
# For Stroke and SatinColumn, place the one that's
# enabled first. Place dependent tabs first too.
parent.toggle and parent.toggle_checkbox.IsChecked(),
# If multiple tabs are enabled, make sure dependent
# tabs are grouped with the parent.
parent,
# Within parent/dependents, put the parent first.
tab == parent
)
return sort_key
tabs.sort(key=tab_sort_key, reverse=True)
return tabs
def cancel(self):
self.cancelled = True
def effect(self):
app = wx.App()
frame = SettingsFrame(tabs_factory=self.create_tabs, on_cancel=self.cancel)
frame.Show()
app.MainLoop()
if self.cancelled:
# This prevents the superclass from outputting the SVG, because we
# may have modified the DOM.
sys.exit(0)

Wyświetl plik

@ -0,0 +1,391 @@
import sys
import traceback
import os
from threading import Thread
import socket
import errno
import time
import logging
from copy import deepcopy
import wx
import appdirs
import json
import inkex
from .. import _, PIXELS_PER_MM, SVG_GROUP_TAG, translation as inkstitch_translation
from .base import InkstitchExtension
from ..stitch_plan import patches_to_stitch_plan
from ..svg import render_stitch_plan
from ..threads import ThreadCatalog
from jinja2 import Environment, FileSystemLoader, select_autoescape
from datetime import date
import base64
from flask import Flask, request, Response, send_from_directory, jsonify
import webbrowser
import requests
def datetimeformat(value, format='%Y/%m/%d'):
return value.strftime(format)
def defaults_path():
defaults_dir = appdirs.user_config_dir('inkstitch')
if not os.path.exists(defaults_dir):
os.makedirs(defaults_dir)
return os.path.join(defaults_dir, 'print_settings.json')
def load_defaults():
try:
with open(defaults_path(), 'r') as defaults_file:
defaults = json.load(defaults_file)
return defaults
except:
return {}
def save_defaults(defaults):
with open(defaults_path(), 'w') as defaults_file:
json.dump(defaults, defaults_file)
def open_url(url):
# Avoid spurious output from xdg-open. Any output on stdout will crash
# inkscape.
null = open(os.devnull, 'w')
old_stdout = os.dup(sys.stdout.fileno())
os.dup2(null.fileno(), sys.stdout.fileno())
if getattr(sys, 'frozen', False):
# PyInstaller sets LD_LIBRARY_PATH. We need to temporarily clear it
# to avoid confusing xdg-open, which webbrowser will run.
# The following code is adapted from PyInstaller's documentation
# http://pyinstaller.readthedocs.io/en/stable/runtime-information.html
old_environ = dict(os.environ) # make a copy of the environment
lp_key = 'LD_LIBRARY_PATH' # for Linux and *BSD.
lp_orig = os.environ.get(lp_key + '_ORIG') # pyinstaller >= 20160820 has this
if lp_orig is not None:
os.environ[lp_key] = lp_orig # restore the original, unmodified value
else:
os.environ.pop(lp_key, None) # last resort: remove the env var
webbrowser.open(url)
# restore the old environ
os.environ.clear()
os.environ.update(old_environ)
else:
webbrowser.open(url)
# restore file descriptors
os.dup2(old_stdout, sys.stdout.fileno())
os.close(old_stdout)
class PrintPreviewServer(Thread):
def __init__(self, *args, **kwargs):
self.html = kwargs.pop('html')
self.metadata = kwargs.pop('metadata')
self.stitch_plan = kwargs.pop('stitch_plan')
Thread.__init__(self, *args, **kwargs)
self.daemon = True
self.last_request_time = None
self.shutting_down = False
self.__setup_app()
def __set_resources_path(self):
if getattr(sys, 'frozen', False):
self.resources_path = os.path.join(sys._MEIPASS, 'print', 'resources')
else:
self.resources_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'print', 'resources')
def __setup_app(self):
self.__set_resources_path()
self.app = Flask(__name__)
@self.app.before_request
def request_started():
self.last_request_time = time.time()
@self.app.before_first_request
def start_watcher():
self.watcher_thread = Thread(target=self.watch)
self.watcher_thread.daemon = True
self.watcher_thread.start()
@self.app.route('/')
def index():
return self.html
@self.app.route('/shutdown', methods=['POST'])
def shutdown():
self.shutting_down = True
request.environ.get('werkzeug.server.shutdown')()
return _('Closing...') + '<br/><br/>' + _('It is safe to close this window now.')
@self.app.route('/resources/<path:resource>', methods=['GET'])
def resources(resource):
return send_from_directory(self.resources_path, resource, cache_timeout=1)
@self.app.route('/ping')
def ping():
# Javascript is letting us know it's still there. This resets self.last_request_time.
return "pong"
@self.app.route('/printing/start')
def printing_start():
# temporarily turn off the watcher while the print dialog is up,
# because javascript will be frozen
self.last_request_time = None
return "OK"
@self.app.route('/printing/end')
def printing_end():
# nothing to do here -- request_started() will restart the watcher
return "OK"
@self.app.route('/settings/<field_name>', methods=['POST'])
def set_field(field_name):
self.metadata[field_name] = request.json['value']
return "OK"
@self.app.route('/settings/<field_mame>', methods=['GET'])
def get_field(field_name):
return jsonify(self.metadata[field_name])
@self.app.route('/settings', methods=['GET'])
def get_settings():
settings = {}
settings.update(load_defaults())
settings.update(self.metadata)
return jsonify(settings)
@self.app.route('/defaults', methods=['POST'])
def set_defaults():
save_defaults(request.json['value'])
return "OK"
@self.app.route('/palette', methods=['POST'])
def set_palette():
name = request.json['name']
catalog = ThreadCatalog()
palette = catalog.get_palette_by_name(name)
catalog.apply_palette(self.stitch_plan, palette)
# clear any saved color or thread names
for field in self.metadata:
if field.startswith('color-') or field.startswith('thread-'):
del self.metadata[field]
self.metadata['thread-palette'] = name
return "OK"
@self.app.route('/threads', methods=['GET'])
def get_threads():
threads = []
for color_block in self.stitch_plan:
threads.append({
'hex': color_block.color.hex_digits,
'name': color_block.color.name,
'manufacturer': color_block.color.manufacturer,
'number': color_block.color.number,
})
return jsonify(threads)
def stop(self):
# for whatever reason, shutting down only seems possible in
# the context of a flask request, so we'll just make one
requests.post("http://%s:%s/shutdown" % (self.host, self.port))
def watch(self):
try:
while True:
time.sleep(1)
if self.shutting_down:
break
if self.last_request_time is not None and \
(time.time() - self.last_request_time) > 3:
self.stop()
break
except:
# seems like sometimes this thread blows up during shutdown
pass
def disable_logging(self):
logging.getLogger('werkzeug').setLevel(logging.ERROR)
def run(self):
self.disable_logging()
self.host = "127.0.0.1"
self.port = 5000
while True:
try:
self.app.run(self.host, self.port, threaded=True)
except socket.error, e:
if e.errno == errno.EADDRINUSE:
self.port += 1
continue
else:
raise
else:
break
class PrintInfoFrame(wx.Frame):
def __init__(self, *args, **kwargs):
self.print_server = kwargs.pop("print_server")
wx.Frame.__init__(self, *args, **kwargs)
panel = wx.Panel(self)
sizer = wx.BoxSizer(wx.VERTICAL)
text = wx.StaticText(panel, label=_("A print preview has been opened in your web browser. This window will stay open in order to communicate with the JavaScript code running in your browser.\n\nThis window will close after you close the print preview in your browser, or you can close it manually if necessary."))
font = wx.Font(14, wx.DEFAULT, wx.NORMAL, wx.NORMAL)
text.SetFont(font)
sizer.Add(text, proportion=1, flag=wx.ALL|wx.EXPAND, border=20)
stop_button = wx.Button(panel, id=wx.ID_CLOSE)
stop_button.Bind(wx.EVT_BUTTON, self.close_button_clicked)
sizer.Add(stop_button, proportion=0, flag=wx.ALIGN_CENTER|wx.ALL, border=10)
panel.SetSizer(sizer)
panel.Layout()
self.timer = wx.PyTimer(self.__watcher)
self.timer.Start(250)
def close_button_clicked(self, event):
self.print_server.stop()
def __watcher(self):
if not self.print_server.is_alive():
self.timer.Stop()
self.timer = None
self.Destroy()
class Print(InkstitchExtension):
def build_environment(self):
if getattr( sys, 'frozen', False ) :
template_dir = os.path.join(sys._MEIPASS, "print", "templates")
else:
template_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "print", "templates")
env = Environment(
loader = FileSystemLoader(template_dir),
autoescape=select_autoescape(['html', 'xml']),
extensions=['jinja2.ext.i18n']
)
env.filters['datetimeformat'] = datetimeformat
env.install_gettext_translations(inkstitch_translation)
return env
def strip_namespaces(self):
# namespace prefixes seem to trip up HTML, so get rid of them
for element in self.document.iter():
if element.tag[0]=='{':
element.tag = element.tag[element.tag.index('}',1) + 1:]
def effect(self):
# It doesn't really make sense to print just a couple of selected
# objects. It's almost certain they meant to print the whole design.
# If they really wanted to print just a few objects, they could set
# the rest invisible temporarily.
self.selected = {}
if not self.get_elements():
return
self.hide_all_layers()
patches = self.elements_to_patches(self.elements)
stitch_plan = patches_to_stitch_plan(patches)
palette = ThreadCatalog().match_and_apply_palette(stitch_plan, self.get_inkstitch_metadata()['thread-palette'])
render_stitch_plan(self.document.getroot(), stitch_plan)
self.strip_namespaces()
# Now the stitch plan layer will contain a set of groups, each
# corresponding to a color block. We'll create a set of SVG files
# corresponding to each individual color block and a final one
# for all color blocks together.
svg = self.document.getroot()
layers = svg.findall("./g[@{http://www.inkscape.org/namespaces/inkscape}groupmode='layer']")
stitch_plan_layer = svg.find(".//*[@id='__inkstitch_stitch_plan__']")
# First, delete all of the other layers. We don't need them and they'll
# just bulk up the SVG.
for layer in layers:
if layer is not stitch_plan_layer:
svg.remove(layer)
overview_svg = inkex.etree.tostring(self.document)
color_block_groups = stitch_plan_layer.getchildren()
for i, group in enumerate(color_block_groups):
# clear the stitch plan layer
del stitch_plan_layer[:]
# add in just this group
stitch_plan_layer.append(group)
# save an SVG preview
stitch_plan.color_blocks[i].svg_preview = inkex.etree.tostring(self.document)
env = self.build_environment()
template = env.get_template('index.html')
html = template.render(
view = {'client_overview': False, 'client_detailedview': False, 'operator_overview': True, 'operator_detailedview': True},
logo = {'src' : '', 'title' : 'LOGO'},
date = date.today(),
client = "",
job = {
'title': '',
'num_colors': stitch_plan.num_colors,
'num_color_blocks': len(stitch_plan),
'num_stops': stitch_plan.num_stops,
'num_trims': stitch_plan.num_trims,
'dimensions': stitch_plan.dimensions_mm,
'num_stitches': stitch_plan.num_stitches,
'estimated_time': '', # TODO
'estimated_thread': '', # TODO
},
svg_overview = overview_svg,
color_blocks = stitch_plan.color_blocks,
palettes = ThreadCatalog().palette_names(),
selected_palette = palette.name,
)
# We've totally mucked with the SVG. Restore it so that we can save
# metadata into it.
self.document = deepcopy(self.original_document)
print_server = PrintPreviewServer(html=html, metadata=self.get_inkstitch_metadata(), stitch_plan=stitch_plan)
print_server.start()
time.sleep(1)
open_url("http://%s:%s/" % (print_server.host, print_server.port))
app = wx.App()
info_frame = PrintInfoFrame(None, title=_("Ink/Stitch Print"), size=(450, 350), print_server=print_server)
info_frame.Show()
app.MainLoop()

Wyświetl plik

@ -0,0 +1,25 @@
from .base import InkstitchExtension
from ..simulator import EmbroiderySimulator
from ..stitch_plan import patches_to_stitch_plan
class Simulate(InkstitchExtension):
def __init__(self):
InkstitchExtension.__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):
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)
app.SetTopWindow(frame)
frame.Show()
wx.CallAfter(frame.go)
app.MainLoop()

Wyświetl plik

@ -0,0 +1,252 @@
import numpy
import wx
import colorsys
from itertools import izip
from .base import InkstitchExtension
from .. import PIXELS_PER_MM
from ..svg import color_block_to_point_lists
class EmbroiderySimulator(wx.Frame):
def __init__(self, *args, **kwargs):
stitch_plan = kwargs.pop('stitch_plan', 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
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
wx.Frame.__init__(self, *args, **kwargs)
self.panel = wx.Panel(self, wx.ID_ANY)
self.panel.SetFocus()
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.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)
self.timer = None
self.last_pos = None
self.Bind(wx.EVT_CLOSE, self.on_close)
def load(self, stitch_plan=None):
if stitch_plan:
self.mirror = False
self.segments = self._stitch_plan_to_segments(stitch_plan)
else:
return
self.trim_margins()
self.calculate_dimensions()
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
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)
elif keycode == ord("R"):
self.stop()
self.clear()
self.go()
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
def color_to_pen(self, color):
return wx.Pen(color.visible_on_white.rgb)
def _stitch_plan_to_segments(self, stitch_plan):
segments = []
for color_block in stitch_plan:
pen = self.color_to_pen(color_block.color)
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
for start, end in izip(point_list[:-1], point_list[1:]):
segments.append(((start, end), pen))
return segments
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
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
for x, y in self.all_coordinates():
width = max(width, x)
height = max(height, y)
self.width = width
self.height = height
self.scale = min(float(self.max_width) / width, float(self.max_height) / height)
# make room for decorations and the margin
self.scale *= 0.95
def go(self):
self.clear()
self.current_stitch = 0
if not self.timer:
self.timer = wx.PyTimer(self.draw_one_frame)
self.timer.Start(self.frame_period)
def on_close(self, event):
self.stop()
if self.on_close_hook:
self.on_close_hook()
# If we keep a reference here, wx crashes when the process exits.
self.canvas = None
self.Destroy()
def stop(self):
if self.timer:
self.timer.Stop()
def clear(self):
self.dc.SetBackground(wx.Brush('white'))
self.dc.Clear()
self.last_pos = None
self.Refresh()
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
self.SetSize((self.width * self.scale + decorations_width + self.margin * 2,
self.height * self.scale + decorations_height + self.margin * 2))
e.Skip()
def on_paint(self, e):
dc = wx.PaintDC(self.panel)
dc.Blit(0, 0, self.buffer.GetWidth(), self.buffer.GetHeight(), self.dc, 0, 0)
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 draw_one_frame(self):
for i in xrange(self.stitches_per_frame):
try:
((x1, y1), (x2, y2)), color = self.segments[self.current_stitch]
if self.mirror:
y1 = self.height - y1
y2 = self.height - y2
x1 = x1 * self.scale + self.margin
y1 = y1 * self.scale + self.margin
x2 = x2 * self.scale + self.margin
y2 = y2 * self.scale + self.margin
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()

Wyświetl plik

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2018-04-29 21:29-0400\n"
"POT-Creation-Date: 2018-04-29 21:45-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"
@ -156,15 +156,6 @@ msgstr ""
msgid "Unknown unit: %s"
msgstr ""
msgid "No embroiderable paths selected."
msgstr ""
msgid "No embroiderable paths found in document."
msgstr ""
msgid "Tip: use Path -> Object to Path to convert non-paths before embroidering."
msgstr ""
msgid "Stitch Plan"
msgstr ""
@ -323,6 +314,15 @@ msgid ""
"be dashed to indicate running stitch. Any kind of dash will work."
msgstr ""
msgid "No embroiderable paths selected."
msgstr ""
msgid "No embroiderable paths found in document."
msgstr ""
msgid "Tip: use Path -> Object to Path to convert non-paths before embroidering."
msgstr ""
msgid ""
"Unable to autofill. This most often happens because your shape is made "
"up of multiple sections that aren't connected."