kopia lustrzana https://github.com/inkstitch/inkstitch
move extension classes into inkstitch/extensions and add inkstitch.py
rodzic
a42726679a
commit
32695e195a
|
@ -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)
|
|
@ -0,0 +1,5 @@
|
|||
from embroider import Embroider
|
||||
from palettes import Palettes
|
||||
from params import Params
|
||||
from print import Print
|
||||
from simulate import Simulate
|
|
@ -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")
|
|
@ -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)
|
|
@ -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()
|
|
@ -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)
|
|
@ -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()
|
|
@ -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()
|
|
@ -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()
|
20
messages.po
20
messages.po
|
@ -8,7 +8,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: PROJECT VERSION\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2018-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."
|
||||
|
|
Ładowanie…
Reference in New Issue