2016-11-17 20:17:55 +00:00
|
|
|
#!/usr/bin/env python
|
|
|
|
# -*- coding: UTF-8 -*-
|
2016-01-20 08:04:32 +00:00
|
|
|
|
|
|
|
import os
|
2016-11-17 20:17:55 +00:00
|
|
|
import sys
|
|
|
|
import json
|
2016-11-20 02:45:47 +00:00
|
|
|
import traceback
|
2017-12-31 01:54:35 +00:00
|
|
|
import time
|
2017-12-31 02:15:34 +00:00
|
|
|
from threading import Thread, Event
|
2017-12-30 21:05:21 +00:00
|
|
|
from copy import copy
|
2016-11-17 20:17:55 +00:00
|
|
|
import wx
|
|
|
|
from wx.lib.scrolledpanel import ScrolledPanel
|
|
|
|
from collections import defaultdict
|
2018-02-05 03:38:24 +00:00
|
|
|
import inkstitch
|
2016-11-17 20:17:55 +00:00
|
|
|
from functools import partial
|
|
|
|
from itertools import groupby
|
2018-03-31 00:37:11 +00:00
|
|
|
from inkstitch import _
|
|
|
|
from inkstitch.extensions import InkstitchExtension
|
|
|
|
from inkstitch.stitch_plan import patches_to_stitch_plan
|
|
|
|
from inkstitch.elements import EmbroideryElement, Fill, AutoFill, Stroke, SatinColumn
|
|
|
|
from inkstitch.utils import save_stderr, restore_stderr
|
2017-12-30 21:05:21 +00:00
|
|
|
from embroider_simulate import EmbroiderySimulator
|
2016-01-20 08:04:32 +00:00
|
|
|
|
2018-02-05 03:38:24 +00:00
|
|
|
|
2016-11-17 20:17:55 +00:00
|
|
|
def presets_path():
|
|
|
|
try:
|
|
|
|
import appdirs
|
2018-01-03 16:09:18 +00:00
|
|
|
config_path = appdirs.user_config_dir('inkstitch')
|
2016-11-17 20:17:55 +00:00
|
|
|
except ImportError:
|
2018-01-03 16:09:18 +00:00
|
|
|
config_path = os.path.expanduser('~/.inkstitch')
|
2016-11-17 20:17:55 +00:00
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
2018-01-03 16:09:18 +00:00
|
|
|
def confirm_dialog(parent, question, caption = 'ink/stitch'):
|
2016-11-17 20:17:55 +00:00
|
|
|
dlg = wx.MessageDialog(parent, question, caption, wx.YES_NO | wx.ICON_QUESTION)
|
|
|
|
result = dlg.ShowModal() == wx.ID_YES
|
|
|
|
dlg.Destroy()
|
|
|
|
return result
|
|
|
|
|
2016-11-07 00:30:49 +00:00
|
|
|
|
2018-01-03 16:09:18 +00:00
|
|
|
def info_dialog(parent, message, caption = 'ink/stitch'):
|
2016-11-17 20:17:55 +00:00
|
|
|
dlg = wx.MessageDialog(parent, message, caption, wx.OK | wx.ICON_INFORMATION)
|
|
|
|
dlg.ShowModal()
|
|
|
|
dlg.Destroy()
|
|
|
|
|
|
|
|
|
|
|
|
class ParamsTab(ScrolledPanel):
|
2016-01-20 08:04:32 +00:00
|
|
|
def __init__(self, *args, **kwargs):
|
2016-11-17 20:17:55 +00:00
|
|
|
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
|
|
|
|
|
2017-12-30 21:05:21 +00:00
|
|
|
def is_dependent_tab(self):
|
|
|
|
return self.parent_tab is not None
|
|
|
|
|
|
|
|
def enabled(self):
|
2018-01-24 01:13:37 +00:00
|
|
|
if self.toggle_checkbox:
|
|
|
|
return self.toggle_checkbox.IsChecked()
|
|
|
|
else:
|
|
|
|
return True
|
2017-12-30 21:05:21 +00:00
|
|
|
|
2016-11-17 20:17:55 +00:00
|
|
|
def update_toggle_state(self, event=None, notify_pair=True):
|
2017-12-30 21:05:21 +00:00
|
|
|
enable = self.enabled()
|
2016-11-17 20:17:55 +00:00
|
|
|
# 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:
|
2017-12-30 21:05:21 +00:00
|
|
|
self.paired_tab.pair_changed(enable)
|
2016-11-17 20:17:55 +00:00
|
|
|
|
|
|
|
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
|
|
|
|
|
2017-12-30 21:05:21 +00:00
|
|
|
if self.enabled() != new_value:
|
2016-11-17 20:17:55 +00:00
|
|
|
self.set_toggle_state(not value)
|
|
|
|
self.update_toggle_state(notify_pair=False)
|
|
|
|
|
2018-01-29 01:26:54 +00:00
|
|
|
if self.on_change_hook:
|
|
|
|
self.on_change_hook(self)
|
|
|
|
|
2016-11-17 20:17:55 +00:00
|
|
|
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()
|
|
|
|
|
2018-01-29 01:26:54 +00:00
|
|
|
if self.on_change_hook:
|
|
|
|
self.on_change_hook(self)
|
|
|
|
|
2016-11-17 20:17:55 +00:00
|
|
|
def set_toggle_state(self, value):
|
2018-01-24 01:13:37 +00:00
|
|
|
if self.toggle_checkbox:
|
|
|
|
self.toggle_checkbox.SetValue(value)
|
|
|
|
self.changed_inputs.add(self.toggle_checkbox)
|
2016-11-17 20:17:55 +00:00
|
|
|
|
|
|
|
def get_values(self):
|
|
|
|
values = {}
|
|
|
|
|
|
|
|
if self.toggle:
|
2017-12-30 21:05:21 +00:00
|
|
|
checked = self.enabled()
|
2016-11-20 02:45:47 +00:00
|
|
|
if self.toggle_checkbox in self.changed_inputs and not self.toggle.inverse:
|
2016-11-17 20:17:55 +00:00
|
|
|
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():
|
2018-01-24 01:13:37 +00:00
|
|
|
if input in self.changed_inputs and input != self.toggle_checkbox:
|
2016-11-17 20:17:55 +00:00
|
|
|
values[name] = input.GetValue()
|
|
|
|
|
|
|
|
return values
|
|
|
|
|
|
|
|
def apply(self):
|
|
|
|
values = self.get_values()
|
|
|
|
for node in self.nodes:
|
2018-01-24 01:13:37 +00:00
|
|
|
# print >> sys.stderr, "apply: ", self.name, node.id, values
|
2016-11-17 20:17:55 +00:00
|
|
|
for name, value in values.iteritems():
|
|
|
|
node.set_param(name, value)
|
|
|
|
|
2017-12-30 21:05:21 +00:00
|
|
|
def on_change(self, callable):
|
|
|
|
self.on_change_hook = callable
|
|
|
|
|
2016-11-17 20:17:55 +00:00
|
|
|
def changed(self, event):
|
|
|
|
self.changed_inputs.add(event.GetEventObject())
|
|
|
|
event.Skip()
|
|
|
|
|
2017-12-30 21:05:21 +00:00
|
|
|
if self.on_change_hook:
|
|
|
|
self.on_change_hook(self)
|
|
|
|
|
2016-11-17 20:17:55 +00:00
|
|
|
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):
|
2018-02-05 03:38:24 +00:00
|
|
|
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)
|
2016-11-17 20:17:55 +00:00
|
|
|
|
|
|
|
if any(len(param.values) > 1 for param in self.params):
|
2018-02-05 03:38:24 +00:00
|
|
|
description += "\n • " + _("Some settings had different values across objects. Select a value from the dropdown or enter a new one.")
|
2016-11-17 20:17:55 +00:00
|
|
|
|
|
|
|
if self.dependent_tabs:
|
2018-02-05 03:38:24 +00:00
|
|
|
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.")
|
2016-11-17 20:17:55 +00:00
|
|
|
|
|
|
|
if self.paired_tab:
|
2018-02-05 03:38:24 +00:00
|
|
|
description += "\n • " + _("Enabling this tab will disable %s and vice-versa.") % self.paired_tab.name
|
2016-11-17 20:17:55 +00:00
|
|
|
|
|
|
|
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)
|
|
|
|
|
2018-02-05 03:38:24 +00:00
|
|
|
summary_box = wx.StaticBox(self, wx.ID_ANY, label=_("Inkscape objects"))
|
2016-11-17 20:17:55 +00:00
|
|
|
sizer = wx.StaticBoxSizer(summary_box, wx.HORIZONTAL)
|
|
|
|
# sizer = wx.BoxSizer(wx.HORIZONTAL)
|
2018-02-20 02:43:39 +00:00
|
|
|
self.description = wx.StaticText(self)
|
2016-11-17 20:17:55 +00:00
|
|
|
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)
|
2016-10-29 17:28:37 +00:00
|
|
|
|
2016-01-20 08:04:32 +00:00
|
|
|
for param in self.params:
|
2018-01-24 01:13:37 +00:00
|
|
|
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)
|
2016-11-17 20:17:55 +00:00
|
|
|
|
|
|
|
if param.type == 'boolean':
|
|
|
|
|
2017-01-05 03:36:20 +00:00
|
|
|
if len(param.values) > 1:
|
2016-11-17 20:17:55 +00:00
|
|
|
input = wx.CheckBox(self, style=wx.CHK_3STATE)
|
|
|
|
input.Set3StateValue(wx.CHK_UNDETERMINED)
|
2016-11-20 03:52:33 +00:00
|
|
|
else:
|
2016-11-17 20:17:55 +00:00
|
|
|
input = wx.CheckBox(self)
|
2017-01-05 03:36:20 +00:00
|
|
|
if param.values:
|
|
|
|
input.SetValue(param.values[0])
|
2016-11-17 20:17:55 +00:00
|
|
|
|
|
|
|
input.Bind(wx.EVT_CHECKBOX, self.changed)
|
|
|
|
elif len(param.values) > 1:
|
2017-12-28 02:39:12 +00:00
|
|
|
input = wx.ComboBox(self, wx.ID_ANY, choices=sorted(param.values), style=wx.CB_DROPDOWN)
|
2016-11-17 20:17:55 +00:00
|
|
|
input.Bind(wx.EVT_COMBOBOX, self.changed)
|
|
|
|
input.Bind(wx.EVT_TEXT, self.changed)
|
|
|
|
else:
|
|
|
|
value = param.values[0] if param.values else ""
|
Fix simulate (#42)
* Simulate now works regardless of the output format you chose when you ran Embroider.
* Simulate (and the preview in Params) now respects TRIMs.
* Inkscape restart required (embroider.inx changed).
This one kind of grew in the telling. #37 was a theoretically simple bug, but in reality, the code necessary to fix it was the straw that broke the camel's back, and I had to do a fair bit of (much needed) code reorganization. Mostly the reorganization was just under the hood, but there was one user-facing change around the Embroider extension's settings window.
Way back in the day, the only way to control things like the stitch length or satin density was through global options specified in the extension settings. We've long since moved to per-object params, but for backward compatibility, ink/stitch defaulted to the command-line arguments.
That means that it was possible to get different stitch results from the same SVG file if you changed the extension's settings. For that reason, I never touched mine. I didn't intend for my users to use those extension-level settings at all, and I've planned to remove those settings for awhile now.
At this point, the extension settings just getting in the way of implementing more features, so I'm getting rid of them and moving the defaults into the parameters system. I've still left things like the output format and the collapse length (although I'm considering moving that one too).
2018-01-28 21:10:37 +00:00
|
|
|
input = wx.TextCtrl(self, wx.ID_ANY, value=str(value))
|
2016-11-17 20:17:55 +00:00
|
|
|
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', [])
|
2017-12-31 02:30:53 +00:00
|
|
|
self.cancel_hook = kwargs.pop('on_cancel', None)
|
2017-12-29 00:55:04 +00:00
|
|
|
wx.Frame.__init__(self, None, wx.ID_ANY,
|
2018-02-05 03:38:24 +00:00
|
|
|
_("Embroidery Params")
|
2016-11-17 20:17:55 +00:00
|
|
|
)
|
|
|
|
self.notebook = wx.Notebook(self, wx.ID_ANY)
|
|
|
|
self.tabs = self.tabs_factory(self.notebook)
|
|
|
|
|
2017-12-30 21:05:21 +00:00
|
|
|
for tab in self.tabs:
|
2018-01-01 03:10:10 +00:00
|
|
|
tab.on_change(self.update_simulator)
|
2017-12-30 21:05:21 +00:00
|
|
|
|
|
|
|
self.simulate_window = None
|
2017-12-31 01:54:35 +00:00
|
|
|
self.simulate_thread = None
|
2017-12-31 02:15:34 +00:00
|
|
|
self.simulate_refresh_needed = Event()
|
2017-12-30 21:05:21 +00:00
|
|
|
|
2018-01-01 03:10:10 +00:00
|
|
|
wx.CallLater(1000, self.update_simulator)
|
|
|
|
|
2018-02-05 03:38:24 +00:00
|
|
|
self.presets_box = wx.StaticBox(self, wx.ID_ANY, label=_("Presets"))
|
2016-11-17 20:17:55 +00:00
|
|
|
|
2017-12-28 02:39:12 +00:00
|
|
|
self.preset_chooser = wx.ComboBox(self, wx.ID_ANY)
|
2016-11-17 20:17:55 +00:00
|
|
|
self.update_preset_list()
|
|
|
|
|
2018-02-05 03:38:24 +00:00
|
|
|
self.load_preset_button = wx.Button(self, wx.ID_ANY, _("Load"))
|
2016-11-17 20:17:55 +00:00
|
|
|
self.load_preset_button.Bind(wx.EVT_BUTTON, self.load_preset)
|
|
|
|
|
2018-02-05 03:38:24 +00:00
|
|
|
self.add_preset_button = wx.Button(self, wx.ID_ANY, _("Add"))
|
2016-11-17 20:17:55 +00:00
|
|
|
self.add_preset_button.Bind(wx.EVT_BUTTON, self.add_preset)
|
|
|
|
|
2018-02-05 03:38:24 +00:00
|
|
|
self.overwrite_preset_button = wx.Button(self, wx.ID_ANY, _("Overwrite"))
|
2016-11-17 20:17:55 +00:00
|
|
|
self.overwrite_preset_button.Bind(wx.EVT_BUTTON, self.overwrite_preset)
|
|
|
|
|
2018-02-05 03:38:24 +00:00
|
|
|
self.delete_preset_button = wx.Button(self, wx.ID_ANY, _("Delete"))
|
2016-11-17 20:17:55 +00:00
|
|
|
self.delete_preset_button.Bind(wx.EVT_BUTTON, self.delete_preset)
|
|
|
|
|
2018-02-05 03:38:24 +00:00
|
|
|
self.cancel_button = wx.Button(self, wx.ID_ANY, _("Cancel"))
|
2017-12-31 02:30:53 +00:00
|
|
|
self.cancel_button.Bind(wx.EVT_BUTTON, self.cancel)
|
|
|
|
self.Bind(wx.EVT_CLOSE, self.cancel)
|
2016-11-17 20:17:55 +00:00
|
|
|
|
2018-02-05 03:38:24 +00:00
|
|
|
self.use_last_button = wx.Button(self, wx.ID_ANY, _("Use Last Settings"))
|
2017-06-13 23:43:42 +00:00
|
|
|
self.use_last_button.Bind(wx.EVT_BUTTON, self.use_last)
|
|
|
|
|
2018-02-05 03:38:24 +00:00
|
|
|
self.apply_button = wx.Button(self, wx.ID_ANY, _("Apply and Quit"))
|
2016-11-17 20:17:55 +00:00
|
|
|
self.apply_button.Bind(wx.EVT_BUTTON, self.apply)
|
|
|
|
|
|
|
|
self.__set_properties()
|
|
|
|
self.__do_layout()
|
|
|
|
# end wxGlade
|
|
|
|
|
2018-01-01 03:10:10 +00:00
|
|
|
def update_simulator(self, tab=None):
|
2017-12-31 01:54:35 +00:00
|
|
|
if self.simulate_window:
|
|
|
|
self.simulate_window.stop()
|
|
|
|
self.simulate_window.clear()
|
|
|
|
|
2017-12-31 02:15:34 +00:00
|
|
|
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()
|
2017-12-31 01:54:35 +00:00
|
|
|
|
2017-12-31 02:15:34 +00:00
|
|
|
self.simulate_refresh_needed.set()
|
|
|
|
|
|
|
|
def simulate_worker(self):
|
|
|
|
while True:
|
|
|
|
self.simulate_refresh_needed.wait()
|
|
|
|
self.simulate_refresh_needed.clear()
|
|
|
|
self.update_patches()
|
2017-12-31 01:54:35 +00:00
|
|
|
|
|
|
|
def update_patches(self):
|
2017-12-30 21:05:21 +00:00
|
|
|
patches = self.generate_patches()
|
|
|
|
|
2018-01-01 03:28:43 +00:00
|
|
|
if patches and not self.simulate_refresh_needed.is_set():
|
2018-01-01 03:10:10 +00:00
|
|
|
wx.CallAfter(self.refresh_simulator, patches)
|
2017-12-30 21:05:21 +00:00
|
|
|
|
2018-01-01 03:10:10 +00:00
|
|
|
def refresh_simulator(self, patches):
|
2018-03-31 00:37:11 +00:00
|
|
|
stitch_plan = patches_to_stitch_plan(patches)
|
2017-12-30 21:05:21 +00:00
|
|
|
if self.simulate_window:
|
|
|
|
self.simulate_window.stop()
|
2018-03-31 00:37:11 +00:00
|
|
|
self.simulate_window.load(stitch_plan=stitch_plan)
|
2017-12-30 21:05:21 +00:00
|
|
|
else:
|
|
|
|
my_rect = self.GetRect()
|
|
|
|
simulator_pos = my_rect.GetTopRight()
|
|
|
|
simulator_pos.x += 5
|
|
|
|
|
2018-01-06 20:48:36 +00:00
|
|
|
screen_rect = wx.Display(0).ClientArea
|
|
|
|
max_width = screen_rect.GetWidth() - my_rect.GetWidth()
|
|
|
|
max_height = screen_rect.GetHeight()
|
|
|
|
|
2017-12-30 21:05:21 +00:00
|
|
|
try:
|
2018-02-05 03:38:24 +00:00
|
|
|
self.simulate_window = EmbroiderySimulator(None, -1, _("Preview"),
|
2018-01-06 20:48:36 +00:00
|
|
|
simulator_pos,
|
|
|
|
size=(300, 300),
|
2018-03-31 00:37:11 +00:00
|
|
|
stitch_plan=stitch_plan,
|
2018-01-06 20:48:36 +00:00
|
|
|
on_close=self.simulate_window_closed,
|
|
|
|
target_duration=5,
|
|
|
|
max_width=max_width,
|
|
|
|
max_height=max_height)
|
2017-12-30 21:05:21 +00:00
|
|
|
except:
|
2018-01-01 03:53:29 +00:00
|
|
|
error = traceback.format_exc()
|
|
|
|
|
|
|
|
try:
|
|
|
|
# a window may have been created, so we need to destroy it
|
|
|
|
# or the app will never exit
|
2018-02-05 03:38:24 +00:00
|
|
|
wx.Window.FindWindowByName("Preview").Destroy()
|
2018-01-01 03:53:29 +00:00
|
|
|
except:
|
|
|
|
pass
|
|
|
|
|
2018-02-05 03:38:24 +00:00
|
|
|
info_dialog(self, error, _("Internal Error"))
|
2017-12-30 21:05:21 +00:00
|
|
|
|
|
|
|
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 = []
|
2018-01-01 19:56:27 +00:00
|
|
|
nodes = []
|
2017-12-30 21:05:21 +00:00
|
|
|
|
|
|
|
for tab in self.tabs:
|
|
|
|
tab.apply()
|
|
|
|
|
2018-01-01 19:56:27 +00:00
|
|
|
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.
|
|
|
|
|
2018-01-24 01:13:37 +00:00
|
|
|
patches.extend(copy(node).embroider(None))
|
2018-01-01 19:56:27 +00:00
|
|
|
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
|
2017-12-30 21:05:21 +00:00
|
|
|
|
|
|
|
return patches
|
|
|
|
|
2016-11-17 20:17:55 +00:00
|
|
|
def update_preset_list(self):
|
2017-06-13 23:43:42 +00:00
|
|
|
preset_names = load_presets().keys()
|
|
|
|
preset_names = [preset for preset in preset_names if preset != "__LAST__"]
|
2017-12-28 02:39:12 +00:00
|
|
|
self.preset_chooser.SetItems(sorted(preset_names))
|
2016-11-17 20:17:55 +00:00
|
|
|
|
|
|
|
def get_preset_name(self):
|
|
|
|
preset_name = self.preset_chooser.GetValue().strip()
|
|
|
|
if preset_name:
|
|
|
|
return preset_name
|
|
|
|
else:
|
2018-02-05 03:38:24 +00:00
|
|
|
info_dialog(self, _("Please enter or select a preset name first."), caption=_('Preset'))
|
2016-11-17 20:17:55 +00:00
|
|
|
return
|
|
|
|
|
|
|
|
def check_and_load_preset(self, preset_name):
|
|
|
|
preset = load_preset(preset_name)
|
|
|
|
if not preset:
|
2018-02-05 03:38:24 +00:00
|
|
|
info_dialog(self, _('Preset "%s" not found.') % preset_name, caption=_('Preset'))
|
2016-11-17 20:17:55 +00:00
|
|
|
|
|
|
|
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)
|
2017-09-27 00:17:06 +00:00
|
|
|
tabs.extend(current_tab.paired_tab.dependent_tabs)
|
2016-11-17 20:17:55 +00:00
|
|
|
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):
|
2018-02-05 03:38:24 +00:00
|
|
|
info_dialog(self, _('Preset "%s" already exists. Please use another name or press "Overwrite"') % preset_name, caption=_('Preset'))
|
2016-11-17 20:17:55 +00:00
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
2017-06-13 23:43:42 +00:00
|
|
|
def _load_preset(self, preset_name):
|
2017-12-29 00:55:04 +00:00
|
|
|
preset = self.check_and_load_preset(preset_name)
|
2016-11-17 20:17:55 +00:00
|
|
|
if not preset:
|
|
|
|
return
|
2017-12-29 00:55:04 +00:00
|
|
|
|
2016-11-17 20:17:55 +00:00
|
|
|
for tab in self.tabs:
|
|
|
|
tab.load_preset(preset)
|
|
|
|
|
2017-06-13 23:43:42 +00:00
|
|
|
|
|
|
|
def load_preset(self, event):
|
|
|
|
preset_name = self.get_preset_name()
|
|
|
|
if not preset_name:
|
|
|
|
return
|
|
|
|
|
|
|
|
self._load_preset(preset_name)
|
|
|
|
|
2016-11-17 20:17:55 +00:00
|
|
|
event.Skip()
|
|
|
|
|
2017-06-13 23:43:42 +00:00
|
|
|
|
2016-11-17 20:17:55 +00:00
|
|
|
def delete_preset(self, event):
|
|
|
|
preset_name = self.get_preset_name()
|
|
|
|
if not preset_name:
|
|
|
|
return
|
|
|
|
|
2017-12-29 00:55:04 +00:00
|
|
|
preset = self.check_and_load_preset(preset_name)
|
2016-11-17 20:17:55 +00:00
|
|
|
if not preset:
|
|
|
|
return
|
|
|
|
|
2017-12-29 00:55:04 +00:00
|
|
|
delete_preset(preset_name)
|
2016-11-17 20:17:55 +00:00
|
|
|
self.update_preset_list()
|
|
|
|
self.preset_chooser.SetValue("")
|
|
|
|
|
|
|
|
event.Skip()
|
|
|
|
|
2017-12-30 21:05:21 +00:00
|
|
|
def _apply(self):
|
2016-11-17 20:17:55 +00:00
|
|
|
for tab in self.tabs:
|
|
|
|
tab.apply()
|
|
|
|
|
2017-12-30 21:05:21 +00:00
|
|
|
def apply(self, event):
|
|
|
|
self._apply()
|
2017-06-13 23:43:42 +00:00
|
|
|
save_preset("__LAST__", self.get_preset_data())
|
2017-12-31 02:30:53 +00:00
|
|
|
self.close()
|
2016-11-17 20:17:55 +00:00
|
|
|
|
2017-06-13 23:43:42 +00:00
|
|
|
def use_last(self, event):
|
|
|
|
self._load_preset("__LAST__")
|
|
|
|
self.apply(event)
|
|
|
|
|
2017-12-31 02:30:53 +00:00
|
|
|
def close(self):
|
2017-12-30 21:05:21 +00:00
|
|
|
if self.simulate_window:
|
|
|
|
self.simulate_window.stop()
|
|
|
|
self.simulate_window.Close()
|
|
|
|
|
2017-12-31 02:30:53 +00:00
|
|
|
self.Destroy()
|
|
|
|
|
|
|
|
def cancel(self, event):
|
|
|
|
if self.cancel_hook:
|
|
|
|
self.cancel_hook()
|
|
|
|
|
|
|
|
self.close()
|
2016-11-17 20:17:55 +00:00
|
|
|
|
|
|
|
def __set_properties(self):
|
|
|
|
# begin wxGlade: MyFrame.__set_properties
|
2018-01-30 01:09:34 +00:00
|
|
|
self.notebook.SetMinSize((800, 600))
|
2016-11-17 20:17:55 +00:00
|
|
|
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)
|
2016-11-20 02:45:47 +00:00
|
|
|
sizer_2.Add(self.preset_chooser, 1, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5)
|
2016-11-17 20:17:55 +00:00
|
|
|
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)
|
2017-06-13 23:43:42 +00:00
|
|
|
sizer_3.Add(self.use_last_button, 0, wx.ALIGN_RIGHT|wx.RIGHT|wx.BOTTOM, 5)
|
2016-11-17 20:17:55 +00:00
|
|
|
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
|
|
|
|
|
2018-03-31 00:37:11 +00:00
|
|
|
class EmbroiderParams(InkstitchExtension):
|
2017-12-31 02:30:53 +00:00
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
self.cancelled = False
|
2018-03-31 00:37:11 +00:00
|
|
|
InkstitchExtension.__init__(self, *args, **kwargs)
|
2017-12-31 02:30:53 +00:00
|
|
|
|
2016-11-17 20:17:55 +00:00
|
|
|
def embroidery_classes(self, node):
|
|
|
|
element = EmbroideryElement(node)
|
|
|
|
classes = []
|
|
|
|
|
|
|
|
if element.get_style("fill"):
|
|
|
|
classes.append(AutoFill)
|
|
|
|
classes.append(Fill)
|
2016-12-24 19:18:41 +00:00
|
|
|
|
|
|
|
if element.get_style("stroke"):
|
2016-11-17 20:17:55 +00:00
|
|
|
classes.append(Stroke)
|
|
|
|
|
|
|
|
if element.get_style("stroke-dasharray") is None:
|
|
|
|
classes.append(SatinColumn)
|
|
|
|
|
|
|
|
return classes
|
|
|
|
|
|
|
|
def get_nodes_by_class(self):
|
2018-03-31 00:37:11 +00:00
|
|
|
nodes = self.get_nodes()
|
2016-11-17 20:17:55 +00:00
|
|
|
nodes_by_class = defaultdict(list)
|
|
|
|
|
Fix simulate (#42)
* Simulate now works regardless of the output format you chose when you ran Embroider.
* Simulate (and the preview in Params) now respects TRIMs.
* Inkscape restart required (embroider.inx changed).
This one kind of grew in the telling. #37 was a theoretically simple bug, but in reality, the code necessary to fix it was the straw that broke the camel's back, and I had to do a fair bit of (much needed) code reorganization. Mostly the reorganization was just under the hood, but there was one user-facing change around the Embroider extension's settings window.
Way back in the day, the only way to control things like the stitch length or satin density was through global options specified in the extension settings. We've long since moved to per-object params, but for backward compatibility, ink/stitch defaulted to the command-line arguments.
That means that it was possible to get different stitch results from the same SVG file if you changed the extension's settings. For that reason, I never touched mine. I didn't intend for my users to use those extension-level settings at all, and I've planned to remove those settings for awhile now.
At this point, the extension settings just getting in the way of implementing more features, so I'm getting rid of them and moving the defaults into the parameters system. I've still left things like the output format and the collapse length (although I'm considering moving that one too).
2018-01-28 21:10:37 +00:00
|
|
|
for z, node in enumerate(nodes):
|
2016-11-17 20:17:55 +00:00
|
|
|
for cls in self.embroidery_classes(node):
|
2018-01-01 19:56:27 +00:00
|
|
|
element = cls(node)
|
|
|
|
element.order = z
|
|
|
|
nodes_by_class[cls].append(element)
|
2016-11-17 20:17:55 +00:00
|
|
|
|
|
|
|
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'
|
2016-11-20 02:45:47 +00:00
|
|
|
else:
|
|
|
|
getter = 'get_param'
|
2016-11-17 20:17:55 +00:00
|
|
|
|
2017-12-29 00:55:04 +00:00
|
|
|
values = filter(lambda item: item is not None,
|
2018-01-30 01:09:34 +00:00
|
|
|
(getattr(node, getter)(param.name, str(param.default)) for node in nodes))
|
2016-11-17 20:17:55 +00:00
|
|
|
|
|
|
|
return values
|
|
|
|
|
|
|
|
def group_params(self, params):
|
2018-01-24 01:13:37 +00:00
|
|
|
def by_group_and_sort_index(param):
|
|
|
|
return param.group, param.sort_index
|
|
|
|
|
2016-11-17 20:17:55 +00:00
|
|
|
def by_group(param):
|
|
|
|
return param.group
|
|
|
|
|
2018-01-24 01:13:37 +00:00
|
|
|
return groupby(sorted(params, key=by_group_and_sort_index), by_group)
|
2016-11-17 20:17:55 +00:00
|
|
|
|
|
|
|
def create_tabs(self, parent):
|
|
|
|
tabs = []
|
|
|
|
for cls, nodes in self.get_nodes_by_class():
|
|
|
|
params = cls.get_params()
|
|
|
|
|
|
|
|
for param in params:
|
2017-01-05 03:36:20 +00:00
|
|
|
param.values = list(set(self.get_values(param, nodes)))
|
2016-11-17 20:17:55 +00:00
|
|
|
|
|
|
|
parent_tab = None
|
|
|
|
new_tabs = []
|
|
|
|
for group, params in self.group_params(params):
|
2018-02-25 01:28:51 +00:00
|
|
|
tab = ParamsTab(parent, id=wx.ID_ANY, name=group or cls.element_name, params=list(params), nodes=nodes)
|
2016-11-17 20:17:55 +00:00
|
|
|
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)
|
|
|
|
|
2016-11-20 02:45:47 +00:00
|
|
|
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)
|
|
|
|
|
2016-11-17 20:17:55 +00:00
|
|
|
return tabs
|
|
|
|
|
2016-01-20 08:04:32 +00:00
|
|
|
|
2017-12-31 02:30:53 +00:00
|
|
|
def cancel(self):
|
|
|
|
self.cancelled = True
|
|
|
|
|
2016-11-07 00:30:49 +00:00
|
|
|
def effect(self):
|
2016-11-17 20:17:55 +00:00
|
|
|
app = wx.App()
|
2017-12-31 02:30:53 +00:00
|
|
|
frame = SettingsFrame(tabs_factory=self.create_tabs, on_cancel=self.cancel)
|
2016-11-17 20:17:55 +00:00
|
|
|
frame.Show()
|
|
|
|
app.MainLoop()
|
|
|
|
|
2017-12-31 02:30:53 +00:00
|
|
|
if self.cancelled:
|
|
|
|
# This prevents the superclass from outputting the SVG, because we
|
|
|
|
# may have modified the DOM.
|
|
|
|
sys.exit(0)
|
|
|
|
|
2016-11-20 02:45:47 +00:00
|
|
|
|
|
|
|
# end of class MyFrame
|
|
|
|
if __name__ == "__main__":
|
|
|
|
save_stderr()
|
|
|
|
|
|
|
|
try:
|
|
|
|
e = EmbroiderParams()
|
|
|
|
e.affect()
|
2017-12-31 02:30:53 +00:00
|
|
|
except SystemExit:
|
|
|
|
raise
|
2016-11-20 02:45:47 +00:00
|
|
|
except:
|
|
|
|
traceback.print_exc()
|
|
|
|
|
|
|
|
restore_stderr()
|