diff options
| author | Lex Neva <lexelby@users.noreply.github.com> | 2019-07-04 10:51:22 -0400 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2019-07-04 10:51:22 -0400 |
| commit | 32548e8e51d13d511813ba4e2a4571bd73e440e5 (patch) | |
| tree | a5c1d8e8932048f6f4c02d55e144023fa82b2e8f /lib | |
| parent | 4db305a36dd87dc9fc21034571eaebb078054bd3 (diff) | |
| parent | c51ae9ccb7052e4015b4a3dd3e2817d8625e3ab0 (diff) | |
lettering features (#399)
lettering features
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/commands.py | 147 | ||||
| -rw-r--r-- | lib/extensions/auto_satin.py | 56 | ||||
| -rw-r--r-- | lib/extensions/base.py | 11 | ||||
| -rw-r--r-- | lib/extensions/commands.py | 118 | ||||
| -rw-r--r-- | lib/extensions/layer_commands.py | 4 | ||||
| -rw-r--r-- | lib/extensions/lettering.py | 183 | ||||
| -rw-r--r-- | lib/extensions/object_commands.py | 9 | ||||
| -rw-r--r-- | lib/gui/__init__.py | 1 | ||||
| -rw-r--r-- | lib/gui/presets.py | 11 | ||||
| -rw-r--r-- | lib/gui/simulator.py | 55 | ||||
| -rw-r--r-- | lib/gui/subtitle_combo_box.py | 85 | ||||
| -rw-r--r-- | lib/i18n.py | 28 | ||||
| -rw-r--r-- | lib/lettering/__init__.py | 2 | ||||
| -rw-r--r-- | lib/lettering/font.py | 86 | ||||
| -rw-r--r-- | lib/stitches/auto_satin.py | 121 | ||||
| -rw-r--r-- | lib/svg/__init__.py | 6 | ||||
| -rw-r--r-- | lib/svg/rendering.py (renamed from lib/svg/realistic_rendering.py) | 129 | ||||
| -rw-r--r-- | lib/svg/svg.py | 128 | ||||
| -rw-r--r-- | lib/utils/dotdict.py | 6 |
19 files changed, 793 insertions, 393 deletions
diff --git a/lib/commands.py b/lib/commands.py index 3c739708..8e35d7ee 100644 --- a/lib/commands.py +++ b/lib/commands.py @@ -1,12 +1,19 @@ +from copy import deepcopy +import os +from random import random import sys -import inkex + import cubicsuperpath +import inkex +from shapely import geometry as shgeo import simpletransform -from .svg import apply_transforms, get_node_transform -from .svg.tags import SVG_USE_TAG, SVG_SYMBOL_TAG, CONNECTION_START, CONNECTION_END, XLINK_HREF -from .utils import cache, Point from .i18n import _, N_ +from .svg import apply_transforms, get_node_transform, get_correction_transform, get_document, generate_unique_id +from .svg.tags import SVG_DEFS_TAG, SVG_GROUP_TAG, SVG_PATH_TAG, SVG_USE_TAG, SVG_SYMBOL_TAG, \ + CONNECTION_START, CONNECTION_END, CONNECTOR_TYPE, XLINK_HREF, INKSCAPE_LABEL +from .utils import cache, get_bundled_dir, Point + COMMANDS = { # L10N command attached to an object @@ -228,3 +235,135 @@ def _standalone_commands(svg): def is_command(node): return CONNECTION_START in node.attrib or CONNECTION_END in node.attrib + + +@cache +def symbols_path(): + return os.path.join(get_bundled_dir("symbols"), "inkstitch.svg") + + +@cache +def symbols_svg(): + with open(symbols_path()) as symbols_file: + return inkex.etree.parse(symbols_file) + + +@cache +def symbol_defs(): + return get_defs(symbols_svg()) + + +@cache +def get_defs(document): + defs = document.find(SVG_DEFS_TAG) + + if defs is None: + defs = inkex.etree.SubElement(document, SVG_DEFS_TAG) + + return defs + + +def ensure_symbol(document, command): + """Make sure the command's symbol definition exists in the <svg:defs> tag.""" + + path = "./*[@id='inkstitch_%s']" % command + defs = get_defs(document) + if defs.find(path) is None: + defs.append(deepcopy(symbol_defs().find(path))) + + +def add_group(document, node, command): + return inkex.etree.SubElement( + node.getparent(), + SVG_GROUP_TAG, + { + "id": generate_unique_id(document, "group"), + INKSCAPE_LABEL: _("Ink/Stitch Command") + ": %s" % get_command_description(command), + "transform": get_correction_transform(node) + }) + + +def add_connector(document, symbol, element): + # I'd like it if I could position the connector endpoint nicely but inkscape just + # moves it to the element's center immediately after the extension runs. + start_pos = (symbol.get('x'), symbol.get('y')) + end_pos = element.shape.centroid + + path = inkex.etree.Element(SVG_PATH_TAG, + { + "id": generate_unique_id(document, "connector"), + "d": "M %s,%s %s,%s" % (start_pos[0], start_pos[1], end_pos.x, end_pos.y), + "style": "stroke:#000000;stroke-width:1px;stroke-opacity:0.5;fill:none;", + CONNECTION_START: "#%s" % symbol.get('id'), + CONNECTION_END: "#%s" % element.node.get('id'), + CONNECTOR_TYPE: "polyline", + + # l10n: the name of the line that connects a command to the object it applies to + INKSCAPE_LABEL: _("connector") + }) + + symbol.getparent().insert(0, path) + + +def add_symbol(document, group, command, pos): + return inkex.etree.SubElement(group, SVG_USE_TAG, + { + "id": generate_unique_id(document, "use"), + XLINK_HREF: "#inkstitch_%s" % command, + "height": "100%", + "width": "100%", + "x": str(pos.x), + "y": str(pos.y), + + # l10n: the name of a command symbol (example: scissors icon for trim command) + INKSCAPE_LABEL: _("command marker"), + }) + + +def get_command_pos(element, index, total): + # Put command symbols 30 pixels out from the shape, spaced evenly around it. + + # get a line running 30 pixels out from the shape + outline = element.shape.buffer(30).exterior + + # find the top center point on the outline and start there + top_center = shgeo.Point(outline.centroid.x, outline.bounds[1]) + start_position = outline.project(top_center, normalized=True) + + # pick this item's spot around the outline and perturb it a bit to avoid + # stacking up commands if they add commands multiple times + position = index / float(total) + position += random() * 0.05 + position += start_position + + return outline.interpolate(position, normalized=True) + + +def remove_legacy_param(element, command): + if command == "trim" or command == "stop": + # If they had the old "TRIM after" or "STOP after" attributes set, + # automatically delete them. THe new commands will do the same + # thing. + # + # If we didn't delete these here, then things would get confusing. + # If the user were to delete a "trim" symbol added by this extension + # but the "embroider_trim_after" attribute is still set, then the + # trim would keep happening. + + attribute = "embroider_%s_after" % command + + if attribute in element.node.attrib: + del element.node.attrib[attribute] + + +def add_commands(element, commands): + document = get_document(element.node) + + for i, command in enumerate(commands): + ensure_symbol(document, command) + remove_legacy_param(element, command) + + group = add_group(document, element.node, command) + pos = get_command_pos(element, i, len(commands)) + symbol = add_symbol(document, group, command, pos) + add_connector(document, symbol, element) diff --git a/lib/extensions/auto_satin.py b/lib/extensions/auto_satin.py index f846ac6b..b7cee83b 100644 --- a/lib/extensions/auto_satin.py +++ b/lib/extensions/auto_satin.py @@ -2,11 +2,8 @@ import sys import inkex -from ..elements import SatinColumn from ..i18n import _ from ..stitches.auto_satin import auto_satin -from ..svg import get_correction_transform -from ..svg.tags import SVG_GROUP_TAG, INKSCAPE_LABEL from .commands import CommandsExtension @@ -37,22 +34,6 @@ class AutoSatin(CommandsExtension): if command is not None: return command.target_point - def effect(self): - if not self.check_selection(): - return - - if self.options.preserve_order: - # when preservering order, auto_satin() takes care of putting the - # newly-created elements into the existing group nodes in the SVG - # DOM - new_elements, trim_indices = self.auto_satin() - else: - group = self.create_group() - new_elements, trim_indices = self.auto_satin() - self.add_elements(group, new_elements) - - self.add_trims(new_elements, trim_indices) - def check_selection(self): if not self.get_elements(): return @@ -64,39 +45,10 @@ class AutoSatin(CommandsExtension): return True - def create_group(self): - first = self.elements[0].node - parent = first.getparent() - insert_index = parent.index(first) - group = inkex.etree.Element(SVG_GROUP_TAG, { - "transform": get_correction_transform(parent, child=True) - }) - parent.insert(insert_index, group) - - return group + def effect(self): + if not self.check_selection(): + return - def auto_satin(self): starting_point = self.get_starting_point() ending_point = self.get_ending_point() - return auto_satin(self.elements, self.options.preserve_order, starting_point, ending_point) - - def add_elements(self, group, new_elements): - for i, element in enumerate(new_elements): - if isinstance(element, SatinColumn): - element.node.set("id", self.uniqueId("autosatin")) - - # L10N Label for a satin column created by Auto-Route Satin Columns extension - element.node.set(INKSCAPE_LABEL, _("AutoSatin %d") % (i + 1)) - else: - element.node.set("id", self.uniqueId("autosatinrun")) - - # L10N Label for running stitch (underpathing) created by Auto-Route Satin Columns extension - element.node.set(INKSCAPE_LABEL, _("AutoSatin Running Stitch %d") % (i + 1)) - - group.append(element.node) - - def add_trims(self, new_elements, trim_indices): - if self.options.trim and trim_indices: - self.ensure_symbol("trim") - for i in trim_indices: - self.add_commands(new_elements[i], ["trim"]) + auto_satin(self.elements, self.options.preserve_order, starting_point, ending_point, self.options.trim) diff --git a/lib/extensions/base.py b/lib/extensions/base.py index 165618aa..440a5413 100644 --- a/lib/extensions/base.py +++ b/lib/extensions/base.py @@ -10,6 +10,7 @@ from stringcase import snakecase from ..commands import layer_commands from ..elements import EmbroideryElement, nodes_to_elements from ..i18n import _ +from ..svg import generate_unique_id from ..svg.tags import SVG_GROUP_TAG, INKSCAPE_GROUPMODE, SVG_DEFS_TAG, EMBROIDERABLE_TAGS @@ -195,15 +196,7 @@ class InkstitchExtension(inkex.Effect): def uniqueId(self, prefix, make_new_id=True): """Override inkex.Effect.uniqueId with a nicer naming scheme.""" - i = 1 - while True: - new_id = "%s%d" % (prefix, i) - if new_id not in self.doc_ids: - break - i += 1 - self.doc_ids[new_id] = 1 - - return new_id + return generate_unique_id(self.document, prefix) def parse(self): """Override inkex.Effect.parse to add Ink/Stitch xml namespace""" diff --git a/lib/extensions/commands.py b/lib/extensions/commands.py index 07b450e1..86e291fd 100644 --- a/lib/extensions/commands.py +++ b/lib/extensions/commands.py @@ -1,16 +1,4 @@ -import os -import inkex -from copy import deepcopy -from random import random - - from .base import InkstitchExtension -from ..utils import get_bundled_dir, cache -from ..commands import get_command_description -from ..i18n import _ -from ..svg.tags import SVG_DEFS_TAG, SVG_PATH_TAG, CONNECTION_START, CONNECTION_END, \ - CONNECTOR_TYPE, INKSCAPE_LABEL, SVG_GROUP_TAG, SVG_USE_TAG, XLINK_HREF -from ..svg import get_correction_transform class CommandsExtension(InkstitchExtension): @@ -20,109 +8,3 @@ class CommandsExtension(InkstitchExtension): InkstitchExtension.__init__(self, *args, **kwargs) for command in self.COMMANDS: self.OptionParser.add_option("--%s" % command, type="inkbool") - - @property - def symbols_path(self): - return os.path.join(get_bundled_dir("symbols"), "inkstitch.svg") - - @property - @cache - def symbols_svg(self): - with open(self.symbols_path) as symbols_file: - return inkex.etree.parse(symbols_file) - - @property - @cache - def symbol_defs(self): - return self.symbols_svg.find(SVG_DEFS_TAG) - - @property - @cache - def defs(self): - return self.document.find(SVG_DEFS_TAG) - - def ensure_symbol(self, command): - path = "./*[@id='inkstitch_%s']" % command - if self.defs.find(path) is None: - self.defs.append(deepcopy(self.symbol_defs.find(path))) - - def add_connector(self, symbol, element): - # I'd like it if I could position the connector endpoint nicely but inkscape just - # moves it to the element's center immediately after the extension runs. - start_pos = (symbol.get('x'), symbol.get('y')) - end_pos = element.shape.centroid - - path = inkex.etree.Element(SVG_PATH_TAG, - { - "id": self.uniqueId("connector"), - "d": "M %s,%s %s,%s" % (start_pos[0], start_pos[1], end_pos.x, end_pos.y), - "style": "stroke:#000000;stroke-width:1px;stroke-opacity:0.5;fill:none;", - CONNECTION_START: "#%s" % symbol.get('id'), - CONNECTION_END: "#%s" % element.node.get('id'), - CONNECTOR_TYPE: "polyline", - - # l10n: the name of the line that connects a command to the object it applies to - INKSCAPE_LABEL: _("connector") - } - ) - - symbol.getparent().insert(0, path) - - def get_command_pos(self, element, index, total): - # Put command symbols 30 pixels out from the shape, spaced evenly around it. - - # get a line running 30 pixels out from the shape - outline = element.shape.buffer(30).exterior - - # pick this item's spot arond the outline and perturb it a bit to avoid - # stacking up commands if they run the extension multiple times - position = index / float(total) - position += random() * 0.1 - - return outline.interpolate(position, normalized=True) - - def remove_legacy_param(self, element, command): - if command == "trim" or command == "stop": - # If they had the old "TRIM after" or "STOP after" attributes set, - # automatically delete them. THe new commands will do the same - # thing. - # - # If we didn't delete these here, then things would get confusing. - # If the user were to delete a "trim" symbol added by this extension - # but the "embroider_trim_after" attribute is still set, then the - # trim would keep happening. - - attribute = "embroider_%s_after" % command - - if attribute in element.node.attrib: - del element.node.attrib[attribute] - - def add_commands(self, element, commands): - for i, command in enumerate(commands): - self.remove_legacy_param(element, command) - - pos = self.get_command_pos(element, i, len(commands)) - - group = inkex.etree.SubElement(element.node.getparent(), SVG_GROUP_TAG, - { - "id": self.uniqueId("group"), - INKSCAPE_LABEL: _("Ink/Stitch Command") + ": %s" % get_command_description(command), - "transform": get_correction_transform(element.node) - } - ) - - symbol = inkex.etree.SubElement(group, SVG_USE_TAG, - { - "id": self.uniqueId("use"), - XLINK_HREF: "#inkstitch_%s" % command, - "height": "100%", - "width": "100%", - "x": str(pos.x), - "y": str(pos.y), - - # l10n: the name of a command symbol (example: scissors icon for trim command) - INKSCAPE_LABEL: _("command marker"), - } - ) - - self.add_connector(symbol, element) diff --git a/lib/extensions/layer_commands.py b/lib/extensions/layer_commands.py index 3a746fcf..c124ec95 100644 --- a/lib/extensions/layer_commands.py +++ b/lib/extensions/layer_commands.py @@ -1,6 +1,6 @@ import inkex -from ..commands import LAYER_COMMANDS, get_command_description +from ..commands import LAYER_COMMANDS, get_command_description, ensure_symbol from ..i18n import _ from ..svg import get_correction_transform from ..svg.tags import SVG_USE_TAG, INKSCAPE_LABEL, XLINK_HREF @@ -21,7 +21,7 @@ class LayerCommands(CommandsExtension): correction_transform = get_correction_transform(self.current_layer, child=True) for i, command in enumerate(commands): - self.ensure_symbol(command) + ensure_symbol(command) inkex.etree.SubElement(self.current_layer, SVG_USE_TAG, { diff --git a/lib/extensions/lettering.py b/lib/extensions/lettering.py index b6d67c0b..a2a729b5 100644 --- a/lib/extensions/lettering.py +++ b/lib/extensions/lettering.py @@ -5,19 +5,23 @@ import json import os import sys +import appdirs import inkex import wx from ..elements import nodes_to_elements -from ..gui import PresetsPanel, SimulatorPreview +from ..gui import PresetsPanel, SimulatorPreview, info_dialog, SubtitleComboBox from ..i18n import _ -from ..lettering import Font +from ..lettering import Font, FontError +from ..svg import get_correction_transform from ..svg.tags import SVG_PATH_TAG, SVG_GROUP_TAG, INKSCAPE_LABEL, INKSTITCH_LETTERING -from ..utils import get_bundled_dir, DotDict +from ..utils import get_bundled_dir, DotDict, cache from .commands import CommandsExtension class LetteringFrame(wx.Frame): + DEFAULT_FONT = "small_font" + def __init__(self, *args, **kwargs): # begin wxGlade: MyFrame.__init__ self.group = kwargs.pop('group') @@ -29,23 +33,28 @@ class LetteringFrame(wx.Frame): self.preview = SimulatorPreview(self, target_duration=1) self.presets_panel = PresetsPanel(self) - self.load_settings() - # options self.options_box = wx.StaticBox(self, wx.ID_ANY, label=_("Options")) self.back_and_forth_checkbox = wx.CheckBox(self, label=_("Stitch lines of text back and forth")) - self.back_and_forth_checkbox.SetValue(self.settings.back_and_forth) - self.Bind(wx.EVT_CHECKBOX, lambda event: self.on_change("back_and_forth", event)) + self.back_and_forth_checkbox.Bind(wx.EVT_CHECKBOX, lambda event: self.on_change("back_and_forth", event)) + + self.trim_checkbox = wx.CheckBox(self, label=_("Add trims")) + self.trim_checkbox.Bind(wx.EVT_CHECKBOX, lambda event: self.on_change("trim", event)) # text editor self.text_editor_box = wx.StaticBox(self, wx.ID_ANY, label=_("Text")) - self.font_chooser = wx.ComboBox(self, wx.ID_ANY) self.update_font_list() + self.font_chooser = SubtitleComboBox(self, wx.ID_ANY, choices=self.get_font_names(), + subtitles=self.get_font_descriptions(), style=wx.CB_READONLY) + self.font_chooser.Bind(wx.EVT_COMBOBOX, self.on_font_changed) + + self.scale_spinner = wx.SpinCtrl(self, wx.ID_ANY, min=100, max=100, initial=100) + self.scale_spinner.Bind(wx.EVT_SPINCTRL, lambda event: self.on_change("scale", event)) - self.text_editor = wx.TextCtrl(self, style=wx.TE_MULTILINE | wx.TE_DONTWRAP, value=self.settings.text) - self.Bind(wx.EVT_TEXT, lambda event: self.on_change("text", event)) + self.text_editor = wx.TextCtrl(self, style=wx.TE_MULTILINE | wx.TE_DONTWRAP) + self.text_editor.Bind(wx.EVT_TEXT, lambda event: self.on_change("text", event)) self.cancel_button = wx.Button(self, wx.ID_ANY, _("Cancel")) self.cancel_button.Bind(wx.EVT_BUTTON, self.cancel) @@ -55,23 +64,38 @@ class LetteringFrame(wx.Frame): self.apply_button.Bind(wx.EVT_BUTTON, self.apply) self.__do_layout() - # end wxGlade + + self.load_settings() + self.apply_settings() def load_settings(self): + """Load the settings saved into the SVG group element""" + + self.settings = DotDict({ + "text": u"", + "back_and_forth": True, + "font": None, + "scale": 100 + }) + try: if INKSTITCH_LETTERING in self.group.attrib: - self.settings = DotDict(json.loads(b64decode(self.group.get(INKSTITCH_LETTERING)))) + self.settings.update(json.loads(b64decode(self.group.get(INKSTITCH_LETTERING)))) return except (TypeError, ValueError): pass - self.settings = DotDict({ - "text": u"", - "back_and_forth": True, - "font": "small_font" - }) + def apply_settings(self): + """Make the settings in self.settings visible in the UI.""" + self.back_and_forth_checkbox.SetValue(bool(self.settings.back_and_forth)) + self.trim_checkbox.SetValue(bool(self.settings.trim)) + self.set_initial_font(self.settings.font) + self.text_editor.SetValue(self.settings.text) + self.scale_spinner.SetValue(self.settings.scale) def save_settings(self): + """Save the settings into the SVG group element.""" + # We base64 encode the string before storing it in an XML attribute. # In theory, lxml should properly html-encode the string, using HTML # entities like as necessary. However, we've found that Inkscape @@ -82,19 +106,102 @@ class LetteringFrame(wx.Frame): # https://bugs.launchpad.net/inkscape/+bug/1804346 self.group.set(INKSTITCH_LETTERING, b64encode(json.dumps(self.settings))) + def update_font_list(self): + font_paths = { + get_bundled_dir("fonts"), + os.path.expanduser("~/.inkstitch/fonts"), + os.path.join(appdirs.user_config_dir('inkstitch'), 'fonts'), + } + + self.fonts = {} + self.fonts_by_id = {} + + for font_path in font_paths: + try: + font_dirs = os.listdir(font_path) + except OSError: + continue + + try: + for font_dir in font_dirs: + font = Font(os.path.join(font_path, font_dir)) + self.fonts[font.name] = font + self.fonts_by_id[font.id] = font + except FontError: + pass + + if len(self.fonts) == 0: + info_dialog(self, _("Unable to find any fonts! Please try reinstalling Ink/Stitch.")) + self.cancel() + + def get_font_names(self): + font_names = [font.name for font in self.fonts.itervalues()] + font_names.sort() + + return font_names + + def get_font_descriptions(self): + return {font.name: font.description for font in self.fonts.itervalues()} + + def set_initial_font(self, font_id): + if font_id: + if font_id not in self.fonts_by_id: + message = '''This text was created using the font "%s", but Ink/Stitch can't find that font. ''' \ + '''A default font will be substituted.''' + info_dialog(self, _(message) % font_id) + + try: + self.font_chooser.SetValueByUser(self.fonts_by_id[font_id].name) + except KeyError: + self.font_chooser.SetValueByUser(self.default_font.name) + + self.on_font_changed() + + @property + @cache + def default_font(self): + try: + return self.fonts[self.DEFAULT_FONT] + except KeyError: + return self.fonts.values()[0] + def on_change(self, attribute, event): self.settings[attribute] = event.GetEventObject().GetValue() self.preview.update() + def on_font_changed(self, event=None): + font = self.fonts.get(self.font_chooser.GetValue(), self.default_font) + self.settings.font = font.id + self.scale_spinner.SetRange(int(font.min_scale * 100), int(font.max_scale * 100)) + self.update_preview() + + def update_preview(self, event=None): + self.preview.update() + + def update_lettering(self): + del self.group[:] + + if self.settings.scale == 100: + destination_group = self.group + else: + destination_group = inkex.etree.SubElement(self.group, SVG_GROUP_TAG, { + # L10N The user has chosen to scale the text by some percentage + # (50%, 200%, etc). If you need to use the percentage symbol, + # make sure to double it (%%). + INKSCAPE_LABEL: _("Text scale %s%%") % self.settings.scale + }) + + font = self.fonts.get(self.font_chooser.GetValue(), self.default_font) + font.render_text(self.settings.text, destination_group, back_and_forth=self.settings.back_and_forth, trim=self.settings.trim) + + if self.settings.scale != 100: + destination_group.attrib['transform'] = 'scale(%s)' % (self.settings.scale / 100.0) + def generate_patches(self, abort_early=None): patches = [] - font_path = os.path.join(get_bundled_dir("fonts"), self.settings.font) - font = Font(font_path) - try: - lines = font.render_text(self.settings.text, back_and_forth=self.settings.back_and_forth) - self.group[:] = lines + self.update_lettering() elements = nodes_to_elements(self.group.iterdescendants(SVG_PATH_TAG)) for element in elements: @@ -113,17 +220,17 @@ class LetteringFrame(wx.Frame): return patches - def update_font_list(self): - pass - def get_preset_data(self): # called by self.presets_panel - preset = {} - return preset + settings = dict(self.settings) + del settings["text"] + return settings - def apply_preset_data(self): - # called by self.presets_panel - return + def apply_preset_data(self, preset_data): + settings = DotDict(preset_data) + settings["text"] = self.settings.text + self.settings = settings + self.apply_settings() def get_preset_suite_name(self): # called by self.presets_panel @@ -131,7 +238,7 @@ class LetteringFrame(wx.Frame): def apply(self, event): self.preview.disable() - self.generate_patches() + self.update_lettering() self.save_settings() self.close() @@ -149,11 +256,18 @@ class LetteringFrame(wx.Frame): outer_sizer = wx.BoxSizer(wx.VERTICAL) options_sizer = wx.StaticBoxSizer(self.options_box, wx.VERTICAL) - options_sizer.Add(self.back_and_forth_checkbox, 1, wx.EXPAND | wx.LEFT | wx.TOP | wx.RIGHT | wx.BOTTOM, 10) + options_sizer.Add(self.back_and_forth_checkbox, 1, wx.EXPAND | wx.LEFT | wx.TOP | wx.RIGHT, 5) + options_sizer.Add(self.trim_checkbox, 1, wx.EXPAND | wx.LEFT | wx.TOP | wx.RIGHT | wx.BOTTOM, 5) outer_sizer.Add(options_sizer, 0, wx.EXPAND | wx.LEFT | wx.TOP | wx.RIGHT, 10) + font_sizer = wx.BoxSizer(wx.HORIZONTAL) + font_sizer.Add(self.font_chooser, 1, wx.EXPAND, 0) + font_sizer.Add(wx.StaticText(self, wx.ID_ANY, "Scale"), 0, wx.LEFT | wx.ALIGN_CENTRE_VERTICAL, 20) + font_sizer.Add(self.scale_spinner, 0, wx.LEFT, 10) + font_sizer.Add(wx.StaticText(self, wx.ID_ANY, "%"), 0, wx.LEFT | wx.ALIGN_CENTRE_VERTICAL, 3) + text_editor_sizer = wx.StaticBoxSizer(self.text_editor_box, wx.VERTICAL) - text_editor_sizer.Add(self.font_chooser, 0, wx.ALL, 10) + text_editor_sizer.Add(font_sizer, 0, wx.ALL | wx.EXPAND, 10) text_editor_sizer.Add(self.text_editor, 1, wx.EXPAND | wx.LEFT | wx.RIGHT | wx.BOTTOM, 10) outer_sizer.Add(text_editor_sizer, 1, wx.EXPAND | wx.LEFT | wx.TOP | wx.RIGHT, 10) @@ -210,7 +324,8 @@ class Lettering(CommandsExtension): else: self.ensure_current_layer() return inkex.etree.SubElement(self.current_layer, SVG_GROUP_TAG, { - INKSCAPE_LABEL: _("Ink/Stitch Lettering") + INKSCAPE_LABEL: _("Ink/Stitch Lettering"), + "transform": get_correction_transform(self.current_layer, child=True) }) def effect(self): diff --git a/lib/extensions/object_commands.py b/lib/extensions/object_commands.py index 47fb361d..d33ab2ba 100644 --- a/lib/extensions/object_commands.py +++ b/lib/extensions/object_commands.py @@ -1,8 +1,8 @@ import inkex -from .commands import CommandsExtension -from ..commands import OBJECT_COMMANDS +from ..commands import OBJECT_COMMANDS, add_commands from ..i18n import _ +from .commands import CommandsExtension class ObjectCommands(CommandsExtension): @@ -24,14 +24,11 @@ class ObjectCommands(CommandsExtension): inkex.errormsg(_("Please choose one or more commands to attach.")) return - for command in commands: - self.ensure_symbol(command) - # Each object (node) in the SVG may correspond to multiple Elements of different # types (e.g. stroke + fill). We only want to process each one once. seen_nodes = set() for element in self.elements: if element.node not in seen_nodes: - self.add_commands(element, commands) + add_commands(element, commands) seen_nodes.add(element.node) diff --git a/lib/gui/__init__.py b/lib/gui/__init__.py index 5e249a55..2214db5d 100644 --- a/lib/gui/__init__.py +++ b/lib/gui/__init__.py @@ -2,3 +2,4 @@ from dialogs import info_dialog, confirm_dialog from electron import open_url from presets import PresetsPanel from simulator import EmbroiderySimulator, SimulatorPreview, show_simulator +from subtitle_combo_box import SubtitleComboBox diff --git a/lib/gui/presets.py b/lib/gui/presets.py index 5337d879..bd0b1787 100644 --- a/lib/gui/presets.py +++ b/lib/gui/presets.py @@ -63,11 +63,12 @@ class PresetsPanel(wx.Panel): self.delete_preset_button.Bind(wx.EVT_BUTTON, self.delete_preset) presets_sizer = wx.StaticBoxSizer(self.presets_box, wx.HORIZONTAL) - presets_sizer.Add(self.preset_chooser, 1, wx.ALIGN_CENTER_VERTICAL | wx.ALL, 10) - presets_sizer.Add(self.load_preset_button, 0, wx.ALIGN_CENTER_VERTICAL | wx.ALL, 10) - presets_sizer.Add(self.add_preset_button, 0, wx.ALIGN_CENTER_VERTICAL | wx.ALL, 10) - presets_sizer.Add(self.overwrite_preset_button, 0, wx.ALIGN_CENTER_VERTICAL | wx.ALL, 10) - presets_sizer.Add(self.delete_preset_button, 0, wx.ALIGN_CENTER_VERTICAL | wx.ALL, 10) + self.preset_chooser.SetMinSize((200, -1)) + presets_sizer.Add(self.preset_chooser, 1, wx.ALIGN_CENTER_VERTICAL | wx.LEFT | wx.BOTTOM | wx.EXPAND, 10) + presets_sizer.Add(self.load_preset_button, 0, wx.ALIGN_CENTER_VERTICAL | wx.BOTTOM | wx.LEFT, 10) + presets_sizer.Add(self.add_preset_button, 0, wx.ALIGN_CENTER_VERTICAL | wx.BOTTOM | wx.LEFT, 10) + presets_sizer.Add(self.overwrite_preset_button, 0, wx.ALIGN_CENTER_VERTICAL | wx.BOTTOM | wx.LEFT, 10) + presets_sizer.Add(self.delete_preset_button, 0, wx.ALIGN_CENTER_VERTICAL | wx.BOTTOM | wx.LEFT | wx.RIGHT, 10) self.SetSizerAndFit(presets_sizer) self.Layout() diff --git a/lib/gui/simulator.py b/lib/gui/simulator.py index 25149149..7184a012 100644 --- a/lib/gui/simulator.py +++ b/lib/gui/simulator.py @@ -390,14 +390,51 @@ class DrawingPanel(wx.Panel): self.last_frame_duration = time.time() - start if last_stitch: - x = last_stitch[0] - y = last_stitch[1] - x, y = transform.TransformPoint(float(x), float(y)) - canvas.SetTransform(canvas.CreateMatrix()) - crosshair_radius = 10 - canvas.SetPen(self.black_pen) - canvas.DrawLines(((x - crosshair_radius, y), (x + crosshair_radius, y))) - canvas.DrawLines(((x, y - crosshair_radius), (x, y + crosshair_radius))) + self.draw_crosshair(last_stitch[0], last_stitch[1], canvas, transform) + + self.draw_scale(canvas) + + def draw_crosshair(self, x, y, canvas, transform): + x, y = transform.TransformPoint(float(x), float(y)) + canvas.SetTransform(canvas.CreateMatrix()) + crosshair_radius = 10 + canvas.SetPen(self.black_pen) + canvas.DrawLines(((x - crosshair_radius, y), (x + crosshair_radius, y))) + canvas.DrawLines(((x, y - crosshair_radius), (x, y + crosshair_radius))) + + def draw_scale(self, canvas): + canvas_width, canvas_height = self.GetClientSize() + + one_mm = PIXELS_PER_MM * self.zoom + scale_width = one_mm + max_width = min(canvas_width * 0.5, 300) + + while scale_width > max_width: + scale_width /= 2.0 + + while scale_width < 50: + scale_width += one_mm + + scale_width_mm = scale_width / self.zoom / PIXELS_PER_MM + + # The scale bar looks like this: + # + # | | + # |_____|_____| + + scale_lower_left_x = 20 + scale_lower_left_y = canvas_height - 20 + + canvas.DrawLines(((scale_lower_left_x, scale_lower_left_y - 6), + (scale_lower_left_x, scale_lower_left_y), + (scale_lower_left_x + scale_width / 2.0, scale_lower_left_y), + (scale_lower_left_x + scale_width / 2.0, scale_lower_left_y - 3), + (scale_lower_left_x + scale_width / 2.0, scale_lower_left_y), + (scale_lower_left_x + scale_width, scale_lower_left_y), + (scale_lower_left_x + scale_width, scale_lower_left_y - 5))) + + canvas.SetFont(wx.Font(12, wx.DEFAULT, wx.NORMAL, wx.NORMAL), wx.Colour((0, 0, 0))) + canvas.DrawText("%s mm" % scale_width_mm, scale_lower_left_x, scale_lower_left_y + 5) def draw_needle_penetration_points(self, canvas, pen, stitches): if self.control_panel.nppBtn.GetValue(): @@ -560,7 +597,7 @@ class DrawingPanel(wx.Panel): # If we just change the zoom, the design will appear to move on the # screen. We have to adjust the pan to compensate. We want to keep # the part of the design under the mouse pointer in the same spot - # after we zoom, so that we appar to be zooming centered on the + # after we zoom, so that we appear to be zooming centered on the # mouse pointer. # This will create a matrix that takes a point in the design and diff --git a/lib/gui/subtitle_combo_box.py b/lib/gui/subtitle_combo_box.py new file mode 100644 index 00000000..64c42153 --- /dev/null +++ b/lib/gui/subtitle_combo_box.py @@ -0,0 +1,85 @@ +import wx +import wx.adv +from wx.lib.wordwrap import wordwrap + + +class SubtitleComboBox(wx.adv.OwnerDrawnComboBox): + TITLE_FONT_SIZE = 12 + SUBTITLE_FONT_SIZE = 10 + + # I'd love to make this 12 too, but if I do it seems to get drawn as 10 + # initially no matter what I do. + CONTROL_FONT_SIZE = 12 + + MARGIN = 5 + + def __init__(self, *args, **kwargs): + self.titles = kwargs.get('choices', []) + subtitles = kwargs.pop('subtitles', {}) + self.subtitles = [subtitles.get(title, '') for title in self.titles] + wx.adv.OwnerDrawnComboBox.__init__(self, *args, **kwargs) + + self.control_font = wx.Font(pointSize=self.CONTROL_FONT_SIZE, family=wx.DEFAULT, style=wx.NORMAL, weight=wx.NORMAL) + self.title_font = wx.Font(pointSize=self.TITLE_FONT_SIZE, family=wx.DEFAULT, style=wx.NORMAL, weight=wx.NORMAL) + self.subtitle_font = wx.Font(pointSize=self.SUBTITLE_FONT_SIZE, family=wx.DEFAULT, style=wx.NORMAL, weight=wx.NORMAL) + + def OnMeasureItemWidth(self, item): + # This _should_ allow us to set the width of the combobox to match the + # width of the widest title. In reality, this method is never called + # and I can't figure out why. We just use self.GetSize().GetWidth() + # instead and rely on the parent window to size us appropriately. Ugh. + + title = self.titles[item] + + # technique from https://stackoverflow.com/a/23529463/4249120 + dc = wx.ScreenDC() + dc.SetFont(self.title_font) + + return dc.GetTextExtent(title).GetWidth() + 2 * self.MARGIN + + def OnMeasureItem(self, item): + title = self.titles[item] + subtitle = self.subtitles[item] + + dc = wx.ScreenDC() + dc.SetFont(self.subtitle_font) + wrapped = wordwrap(subtitle, self.GetSize().GetWidth(), dc) + subtitle_height = dc.GetTextExtent(wrapped).GetHeight() + + dc = wx.ScreenDC() + dc.SetFont(self.title_font) + title_height = dc.GetTextExtent(title).GetHeight() + + return subtitle_height + title_height + 3 * self.MARGIN + + def OnDrawBackground(self, dc, rect, item, flags): + if flags & wx.adv.ODCB_PAINTING_SELECTED: + # let the parent class draw the selected item so we don't + # hae to figure out the highlight color + wx.adv.OwnerDrawnComboBox.OnDrawBackground(self, dc, rect, item, flags) + else: + # alternate white and grey for the dropdown items, and draw the + # combo box itself as white + if flags & wx.adv.ODCB_PAINTING_CONTROL or item % 2 == 0: + background_color = wx.Colour(255, 255, 255) + else: + background_color = wx.Colour(240, 240, 240) + + dc.SetBrush(wx.Brush(background_color)) + dc.SetPen(wx.Pen(background_color)) + dc.DrawRectangle(rect) + + def OnDrawItem(self, dc, rect, item, flags): + if flags & wx.adv.ODCB_PAINTING_CONTROL: + # painting the selected item in the box + dc.SetFont(self.control_font) + dc.DrawText(self.titles[item], rect.x + self.MARGIN, rect.y + self.MARGIN) + else: + # painting the items in the popup + dc.SetFont(self.title_font) + title_height = dc.GetCharHeight() + dc.DrawText(self.titles[item], rect.x + self.MARGIN, rect.y + self.MARGIN) + + dc.SetFont(self.subtitle_font) + subtitle = wordwrap(self.subtitles[item], self.GetSize().GetWidth(), dc) + dc.DrawText(subtitle, rect.x + self.MARGIN, rect.y + title_height + self.MARGIN * 2) diff --git a/lib/i18n.py b/lib/i18n.py index 98f63ec1..f57bbf9c 100644 --- a/lib/i18n.py +++ b/lib/i18n.py @@ -1,7 +1,9 @@ -import sys +import gettext import os from os.path import dirname, realpath -import gettext +import sys + +from .utils import cache _ = translation = None locale_dir = None @@ -33,5 +35,27 @@ def localize(languages=None): _ = translation.ugettext +@cache +def get_languages(): + """return a list of languages configured by the user + + I really wish gettext provided this as a function. Instead, we've duplicated + its code below. + """ + + languages = [] + + for envar in ('LANGUAGE', 'LC_ALL', 'LC_MESSAGES', 'LANG'): + val = os.environ.get(envar) + if val: + languages = val.split(':') + break + + if 'C' not in languages: + languages.append('C') + + return languages + + _set_locale_dir() localize() diff --git a/lib/lettering/__init__.py b/lib/lettering/__init__.py index c6201223..5d20d683 100644 --- a/lib/lettering/__init__.py +++ b/lib/lettering/__init__.py @@ -1 +1 @@ -from font import Font
\ No newline at end of file +from font import Font, FontError
\ No newline at end of file diff --git a/lib/lettering/font.py b/lib/lettering/font.py index 9d0389a0..13a2b78c 100644 --- a/lib/lettering/font.py +++ b/lib/lettering/font.py @@ -7,7 +7,8 @@ import os import inkex from ..elements import nodes_to_elements -from ..i18n import _ +from ..exceptions import InkstitchException +from ..i18n import _, get_languages from ..stitches.auto_satin import auto_satin from ..svg import PIXELS_PER_MM from ..svg.tags import SVG_GROUP_TAG, SVG_PATH_TAG, INKSCAPE_LABEL @@ -15,6 +16,10 @@ from ..utils import Point from .font_variant import FontVariant +class FontError(InkstitchException): + pass + + def font_metadata(name, default=None, multiplier=None): def getter(self): value = self.metadata.get(name, default) @@ -27,6 +32,25 @@ def font_metadata(name, default=None, multiplier=None): return property(getter) +def localized_font_metadata(name, default=None): + def getter(self): + # If the font contains a localized version of the attribute, use it. + for language in get_languages(): + attr = "%s_%s" % (name, language) + if attr in self.metadata: + return self.metadata.get(attr) + + if name in self.metadata: + # This may be a font packaged with Ink/Stitch, in which case the + # text will have been sent to CrowdIn for community translation. + # Try to fetch the translated version. + return _(self.metadata.get(name)) + else: + return default + + return property(getter) + + class Font(object): """Represents a font with multiple variants. @@ -43,36 +67,42 @@ class Font(object): def __init__(self, font_path): self.path = font_path + self.metadata = {} + self.license = None + self.variants = {} + self._load_metadata() self._load_license() - self._load_variants() def _load_metadata(self): try: with open(os.path.join(self.path, "font.json")) as metadata_file: self.metadata = json.load(metadata_file) except IOError: - self.metadata = {} + pass def _load_license(self): try: with open(os.path.join(self.path, "LICENSE")) as license_file: self.license = license_file.read() except IOError: - self.license = None + pass def _load_variants(self): - self.variants = {} - - for variant in FontVariant.VARIANT_TYPES: - try: - self.variants[variant] = FontVariant(self.path, variant, self.default_glyph) - except IOError: - # we'll deal with missing variants when we apply lettering - pass - - name = font_metadata('name', '') - description = font_metadata('description', '') + if not self.variants: + for variant in FontVariant.VARIANT_TYPES: + try: + self.variants[variant] = FontVariant(self.path, variant, self.default_glyph) + except IOError: + # we'll deal with missing variants when we apply lettering + pass + + def _check_variants(self): + if self.variants.get(self.default_variant) is None: + raise FontError("font not found or has no default variant") + + name = localized_font_metadata('name', '') + description = localized_font_metadata('description', '') default_variant = font_metadata('default_variant', FontVariant.LEFT_TO_RIGHT) default_glyph = font_metadata('defalt_glyph', u"�") letter_spacing = font_metadata('letter_spacing', 1.5, multiplier=PIXELS_PER_MM) @@ -80,8 +110,17 @@ class Font(object): word_spacing = font_metadata('word_spacing', 3, multiplier=PIXELS_PER_MM) kerning_pairs = font_metadata('kerning_pairs', {}) auto_satin = font_metadata('auto_satin', True) + min_scale = font_metadata('min_scale', 1.0) + max_scale = font_metadata('max_scale', 1.0) + + @property + def id(self): + return os.path.basename(self.path) + + def render_text(self, text, destination_group, variant=None, back_and_forth=True, trim=False): + """Render text into an SVG group element.""" + self._load_variants() - def render_text(self, text, variant=None, back_and_forth=True): if variant is None: variant = self.default_variant @@ -90,9 +129,6 @@ class Font(object): else: glyph_sets = [self.get_variant(variant)] * 2 - line_group = inkex.etree.Element(SVG_GROUP_TAG, { - INKSCAPE_LABEL: _("Ink/Stitch Text") - }) position = Point(0, 0) for i, line in enumerate(text.splitlines()): glyph_set = glyph_sets[i % 2] @@ -101,15 +137,15 @@ class Font(object): letter_group = self._render_line(line, position, glyph_set) if glyph_set.variant == FontVariant.RIGHT_TO_LEFT: letter_group[:] = reversed(letter_group) - line_group.append(letter_group) + destination_group.append(letter_group) position.x = 0 position.y += self.leading - if self.auto_satin and len(line_group) > 0: - self._apply_auto_satin(line_group) + if self.auto_satin and len(destination_group) > 0: + self._apply_auto_satin(destination_group, trim) - return line_group + return destination_group def get_variant(self, variant): return self.variants.get(variant, self.variants[self.default_variant]) @@ -174,7 +210,7 @@ class Font(object): return node - def _apply_auto_satin(self, group): + def _apply_auto_satin(self, group, trim): """Apply Auto-Satin to an SVG XML node tree with an svg:g at its root. The group's contents will be replaced with the results of the auto- @@ -182,4 +218,4 @@ class Font(object): """ elements = nodes_to_elements(group.iterdescendants(SVG_PATH_TAG)) - auto_satin(elements, preserve_order=True) + auto_satin(elements, preserve_order=True, trim=trim) diff --git a/lib/stitches/auto_satin.py b/lib/stitches/auto_satin.py index e204a445..75b13176 100644 --- a/lib/stitches/auto_satin.py +++ b/lib/stitches/auto_satin.py @@ -9,9 +9,11 @@ import simplestyle import networkx as nx +from ..commands import add_commands from ..elements import Stroke, SatinColumn -from ..svg import PIXELS_PER_MM, line_strings_to_csp, get_correction_transform -from ..svg.tags import SVG_PATH_TAG +from ..i18n import _ +from ..svg import PIXELS_PER_MM, line_strings_to_csp, get_correction_transform, generate_unique_id +from ..svg.tags import SVG_PATH_TAG, SVG_GROUP_TAG, INKSCAPE_LABEL from ..utils import Point as InkstitchPoint, cut, cache @@ -258,7 +260,7 @@ class RunningStitch(object): return RunningStitch(new_path, self.original_element) -def auto_satin(elements, preserve_order=False, starting_point=None, ending_point=None): +def auto_satin(elements, preserve_order=False, starting_point=None, ending_point=None, trim=False): """Find an optimal order to stitch a list of SatinColumns. Add running stitch and jump stitches as necessary to construct a stitch @@ -294,14 +296,20 @@ def auto_satin(elements, preserve_order=False, starting_point=None, ending_point If preserve_order is True, then the elements and any newly-created elements will be in the same position in the SVG DOM. If preserve_order is False, then - the elements will be removed from the SVG DOM and it's up to the caller to - decide where to put the returned SVG path nodes. + the elements will be removed from their current position in SVG DOM and added + to a newly-created group node. - Returns: a list of SVG path nodes making up the selected stitch order. + If trim is True, then Trim commands will be added to avoid jump stitches. + + Returns: a list of Element instances making up the stitching order chosen. Jumps between objects are implied if they are not right next to each other. """ + # save these for create_new_group() call below + parent = elements[0].node.getparent() + index = parent.index(elements[0].node) + graph = build_graph(elements, preserve_order) add_jumps(graph, elements, preserve_order) starting_node, ending_node = get_starting_and_ending_nodes( @@ -310,12 +318,21 @@ def auto_satin(elements, preserve_order=False, starting_point=None, ending_point operations = path_to_operations(graph, path) operations = collapse_sequential_segments(operations) new_elements, trims, original_parents = operations_to_elements_and_trims(operations, preserve_order) + remove_original_elements(elements) if preserve_order: preserve_original_groups(new_elements, original_parents) + else: + group = create_new_group(parent, index) + add_elements_to_group(new_elements, group) + + name_elements(new_elements, preserve_order) - return new_elements, trims + if trim: + new_elements = add_trims(new_elements, trims) + + return new_elements def build_graph(elements, preserve_order=False): @@ -596,7 +613,7 @@ def operations_to_elements_and_trims(operations, preserve_order): elements.append(operation.to_element()) original_parent_nodes.append(operation.original_node.getparent()) elif isinstance(operation, (JumpStitch)): - if elements and operation.length > PIXELS_PER_MM: + if elements and operation.length > 0.75 * PIXELS_PER_MM: trims.append(len(elements) - 1) return elements, list(set(trims)), original_parent_nodes @@ -628,3 +645,91 @@ def preserve_original_groups(elements, original_parent_nodes): if parent is not None: parent.append(element.node) element.node.set('transform', get_correction_transform(parent, child=True)) + + +def create_new_group(parent, insert_index): + group = inkex.etree.Element(SVG_GROUP_TAG, { + INKSCAPE_LABEL: _("Auto-Satin"), + "transform": get_correction_transform(parent, child=True) + }) + parent.insert(insert_index, group) + + return group + + +def add_elements_to_group(elements, group): + for element in elements: + group.append(element.node) + + +def name_elements(new_elements, preserve_order): + """Give the newly-created SVG objects useful names. + + Objects will be named like this: + + * AutoSatin 1 + * AutoSatin 2 + * AutoSatin Running Stitch 3 + * AutoSatin 4 + * AutoSatin Running Stitch 5 + ... + + Objects are numbered starting with 1. Satins are named "AutoSatin #", and + running stitches are named "AutoSatin Running Stitch #". + + If preserve_order is true and the element already has an INKSCAPE_LABEL, + we'll leave it alone. That way users can see which original satin the new + satin(s) came from. + + SVG element IDs are also set. Since these need to be unique across the + document, the numbers will likely not match up with the numbers in the + name we set. + """ + + index = 1 + for element in new_elements: + if isinstance(element, SatinColumn): + element.node.set("id", generate_unique_id(element.node, "autosatin")) + else: + element.node.set("id", generate_unique_id(element.node, "autosatinrun")) + + if not (preserve_order and INKSCAPE_LABEL in element.node.attrib): + if isinstance(element, SatinColumn): + # L10N Label for a satin column created by Auto-Route Satin Columns and Lettering extensions + element.node.set(INKSCAPE_LABEL, _("AutoSatin %d") % index) + else: + # L10N Label for running stitch (underpathing) created by Auto-Route Satin Columns amd Lettering extensions + element.node.set(INKSCAPE_LABEL, _("AutoSatin Running Stitch %d") % index) + + index += 1 + + +def add_trims(elements, trim_indices): + """Add trim commands on the specified elements. + + If any running stitches immediately follow a trim, they are eliminated. + When we're trimming, there's no need to try to reduce the jump length, + so the running stitch would be a waste of time (and thread). + """ + + trim_indices = set(trim_indices) + new_elements = [] + just_trimmed = False + for i, element in enumerate(elements): + if just_trimmed and isinstance(element, Stroke): + element.node.getparent().remove(element.node) + continue + + if i in trim_indices: + add_commands(element, ["trim"]) + just_trimmed = True + else: + just_trimmed = False + + new_elements.append(element) + + # trim at the end, too + if i not in trim_indices: + add_commands(element, ["trim"]) + + return new_elements diff --git a/lib/svg/__init__.py b/lib/svg/__init__.py index 34cc4b3d..640aee73 100644 --- a/lib/svg/__init__.py +++ b/lib/svg/__init__.py @@ -1,4 +1,6 @@ from .guides import get_guides from .path import apply_transforms, get_node_transform, get_correction_transform, line_strings_to_csp, point_lists_to_csp, line_strings_to_path -from .svg import color_block_to_point_lists, render_stitch_plan -from .units import * +from .path import apply_transforms, get_node_transform, get_correction_transform, line_strings_to_csp, point_lists_to_csp +from .rendering import color_block_to_point_lists, render_stitch_plan +from .svg import get_document, generate_unique_id +from .units import *
\ No newline at end of file diff --git a/lib/svg/realistic_rendering.py b/lib/svg/rendering.py index 73da3a09..41ed53d7 100644 --- a/lib/svg/realistic_rendering.py +++ b/lib/svg/rendering.py @@ -1,8 +1,17 @@ -import simplepath import math -from .units import PIXELS_PER_MM +import inkex +import simplepath +import simplestyle +import simpletransform + +from ..i18n import _ from ..utils import Point +from ..utils import cache +from .tags import SVG_GROUP_TAG, INKSCAPE_LABEL, INKSCAPE_GROUPMODE, SVG_PATH_TAG, SVG_DEFS_TAG +from .units import PIXELS_PER_MM +from .units import get_viewbox_transform + # The stitch vector path looks like this: # _______ @@ -10,7 +19,6 @@ from ..utils import Point # # It's 0.32mm high, which is the approximate thickness of common machine # embroidery threads. - # 1.216 pixels = 0.32mm stitch_height = 1.216 @@ -128,3 +136,118 @@ def realistic_stitch(start, end): simplepath.translatePath(path, stitch_center.x - rotation_center_x, stitch_center.y - rotation_center_y) return simplepath.formatPath(path) + + +def color_block_to_point_lists(color_block): + point_lists = [[]] + + for stitch in color_block: + if stitch.trim: + if point_lists[-1]: + point_lists.append([]) + continue + + if not stitch.jump and not stitch.color_change: + point_lists[-1].append(stitch.as_tuple()) + + # filter out empty point lists + point_lists = [p for p in point_lists if p] + + return point_lists + + +@cache +def get_correction_transform(svg): + transform = get_viewbox_transform(svg) + + # we need to correct for the viewbox + transform = simpletransform.invertTransform(transform) + transform = simpletransform.formatTransform(transform) + + return transform + + +def color_block_to_realistic_stitches(color_block, svg): + paths = [] + + for point_list in color_block_to_point_lists(color_block): + if not point_list: + continue + + color = color_block.color.visible_on_white.darker.to_hex_str() + start = point_list[0] + for point in point_list[1:]: + paths.append(inkex.etree.Element( + SVG_PATH_TAG, + {'style': simplestyle.formatStyle( + { + 'fill': color, + 'stroke': 'none', + 'filter': 'url(#realistic-stitch-filter)' + }), + 'd': realistic_stitch(start, point), + 'transform': get_correction_transform(svg) + })) + start = point + + return paths + + +def color_block_to_paths(color_block, svg): + paths = [] + # We could emit just a single path with one subpath per point list, but + # emitting multiple paths makes it easier for the user to manipulate them. + for point_list in color_block_to_point_lists(color_block): + color = color_block.color.visible_on_white.to_hex_str() + paths.append(inkex.etree.Element( + SVG_PATH_TAG, + {'style': simplestyle.formatStyle( + {'stroke': color, + 'stroke-width': "0.4", + 'fill': 'none'}), + 'd': "M" + " ".join(" ".join(str(coord) for coord in point) for point in point_list), + 'transform': get_correction_transform(svg), + 'embroider_manual_stitch': 'true', + 'embroider_trim_after': 'true', + })) + + # no need to trim at the end of a thread color + if paths: + paths[-1].attrib.pop('embroider_trim_after') + + return paths + + +def render_stitch_plan(svg, stitch_plan, realistic=False): + layer = svg.find(".//*[@id='__inkstitch_stitch_plan__']") + if layer is None: + layer = inkex.etree.Element(SVG_GROUP_TAG, + {'id': '__inkstitch_stitch_plan__', + INKSCAPE_LABEL: _('Stitch Plan'), + INKSCAPE_GROUPMODE: 'layer'}) + else: + # delete old stitch plan + del layer[:] + + # make sure the layer is visible + layer.set('style', 'display:inline') + + for i, color_block in enumerate(stitch_plan): + group = inkex.etree.SubElement(layer, + SVG_GROUP_TAG, + {'id': '__color_block_%d__' % i, + INKSCAPE_LABEL: "color block %d" % (i + 1)}) + if realistic: + group.extend(color_block_to_realistic_stitches(color_block, svg)) + else: + group.extend(color_block_to_paths(color_block, svg)) + + svg.append(layer) + + if realistic: + defs = svg.find(SVG_DEFS_TAG) + + if defs is None: + defs = inkex.etree.SubElement(svg, SVG_DEFS_TAG) + + defs.append(inkex.etree.fromstring(realistic_filter)) diff --git a/lib/svg/svg.py b/lib/svg/svg.py index 3fceebfb..464a2a18 100644 --- a/lib/svg/svg.py +++ b/lib/svg/svg.py @@ -1,124 +1,26 @@ -import inkex -import simplestyle -import simpletransform +from inkex import etree -from ..i18n import _ from ..utils import cache -from .realistic_rendering import realistic_stitch, realistic_filter -from .tags import SVG_GROUP_TAG, INKSCAPE_LABEL, INKSCAPE_GROUPMODE, SVG_PATH_TAG, SVG_DEFS_TAG -from .units import get_viewbox_transform - - -def color_block_to_point_lists(color_block): - point_lists = [[]] - - for stitch in color_block: - if stitch.trim: - if point_lists[-1]: - point_lists.append([]) - continue - - if not stitch.jump and not stitch.color_change: - point_lists[-1].append(stitch.as_tuple()) - - # filter out empty point lists - point_lists = [p for p in point_lists if p] - - return point_lists @cache -def get_correction_transform(svg): - transform = get_viewbox_transform(svg) - - # we need to correct for the viewbox - transform = simpletransform.invertTransform(transform) - transform = simpletransform.formatTransform(transform) - - return transform - - -def color_block_to_realistic_stitches(color_block, svg): - paths = [] +def get_document(node): + return node.getroottree().getroot() - for point_list in color_block_to_point_lists(color_block): - if not point_list: - continue - color = color_block.color.visible_on_white.darker.to_hex_str() - start = point_list[0] - for point in point_list[1:]: - paths.append(inkex.etree.Element( - SVG_PATH_TAG, - {'style': simplestyle.formatStyle( - { - 'fill': color, - 'stroke': 'none', - 'filter': 'url(#realistic-stitch-filter)' - }), - 'd': realistic_stitch(start, point), - 'transform': get_correction_transform(svg) - })) - start = point - - return paths - - -def color_block_to_paths(color_block, svg): - paths = [] - # We could emit just a single path with one subpath per point list, but - # emitting multiple paths makes it easier for the user to manipulate them. - for point_list in color_block_to_point_lists(color_block): - color = color_block.color.visible_on_white.to_hex_str() - paths.append(inkex.etree.Element( - SVG_PATH_TAG, - {'style': simplestyle.formatStyle( - {'stroke': color, - 'stroke-width': "0.4", - 'fill': 'none'}), - 'd': "M" + " ".join(" ".join(str(coord) for coord in point) for point in point_list), - 'transform': get_correction_transform(svg), - 'embroider_manual_stitch': 'true', - 'embroider_trim_after': 'true', - })) - - # no need to trim at the end of a thread color - if paths: - paths[-1].attrib.pop('embroider_trim_after') - - return paths - - -def render_stitch_plan(svg, stitch_plan, realistic=False): - layer = svg.find(".//*[@id='__inkstitch_stitch_plan__']") - if layer is None: - layer = inkex.etree.Element(SVG_GROUP_TAG, - {'id': '__inkstitch_stitch_plan__', - INKSCAPE_LABEL: _('Stitch Plan'), - INKSCAPE_GROUPMODE: 'layer'}) +def generate_unique_id(document_or_element, prefix="path"): + if isinstance(document_or_element, etree._ElementTree): + document = document_or_element.getroot() else: - # delete old stitch plan - del layer[:] - - # make sure the layer is visible - layer.set('style', 'display:inline') - - for i, color_block in enumerate(stitch_plan): - group = inkex.etree.SubElement(layer, - SVG_GROUP_TAG, - {'id': '__color_block_%d__' % i, - INKSCAPE_LABEL: "color block %d" % (i + 1)}) - if realistic: - group.extend(color_block_to_realistic_stitches(color_block, svg)) - else: - group.extend(color_block_to_paths(color_block, svg)) - - svg.append(layer) + document = get_document(document_or_element) - if realistic: - defs = svg.find(SVG_DEFS_TAG) + doc_ids = {node.get('id') for node in document.iterdescendants() if 'id' in node.attrib} - if defs is None: - defs = inkex.etree.SubElement(svg, SVG_DEFS_TAG) + i = 1 + while True: + new_id = "%s%d" % (prefix, i) + if new_id not in doc_ids: + break + i += 1 - defs.append(inkex.etree.fromstring(realistic_filter)) + return new_id diff --git a/lib/utils/dotdict.py b/lib/utils/dotdict.py index 1ab3a4fe..76f23697 100644 --- a/lib/utils/dotdict.py +++ b/lib/utils/dotdict.py @@ -6,7 +6,13 @@ class DotDict(dict): def __init__(self, *args, **kwargs): super(DotDict, self).__init__(*args, **kwargs) + self._dotdictify() + def update(self, *args, **kwargs): + super(DotDict, self).update(*args, **kwargs) + self.dotdictify() + + def _dotdictify(self): for k, v in self.iteritems(): if isinstance(v, dict): self[k] = DotDict(v) |
