From 003ee406a5bc5d31f5f62183fc5d67b4140a39f9 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Tue, 18 Dec 2018 20:16:54 -0500 Subject: add trim checkbox --- lib/extensions/lettering.py | 17 +++++++++++------ lib/lettering/font.py | 2 +- 2 files changed, 12 insertions(+), 7 deletions(-) (limited to 'lib') diff --git a/lib/extensions/lettering.py b/lib/extensions/lettering.py index b6d67c0b..fd9c7628 100644 --- a/lib/extensions/lettering.py +++ b/lib/extensions/lettering.py @@ -38,6 +38,10 @@ class LetteringFrame(wx.Frame): 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.trim_checkbox = wx.CheckBox(self, label=_("Add trims")) + self.trim_checkbox.SetValue(bool(self.settings.trim)) + self.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")) @@ -86,15 +90,16 @@ class LetteringFrame(wx.Frame): self.settings[attribute] = event.GetEventObject().GetValue() self.preview.update() - def generate_patches(self, abort_early=None): - patches = [] - + def update_lettering(self): font_path = os.path.join(get_bundled_dir("fonts"), self.settings.font) font = Font(font_path) + self.group[:] = font.render_text(self.settings.text, back_and_forth=self.settings.back_and_forth, trim=self.settings.trim) + + def generate_patches(self, abort_early=None): + patches = [] 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: @@ -131,7 +136,7 @@ class LetteringFrame(wx.Frame): def apply(self, event): self.preview.disable() - self.generate_patches() + self.update_lettering() self.save_settings() self.close() diff --git a/lib/lettering/font.py b/lib/lettering/font.py index 9d0389a0..05465c8c 100644 --- a/lib/lettering/font.py +++ b/lib/lettering/font.py @@ -81,7 +81,7 @@ class Font(object): kerning_pairs = font_metadata('kerning_pairs', {}) auto_satin = font_metadata('auto_satin', True) - def render_text(self, text, variant=None, back_and_forth=True): + def render_text(self, text, variant=None, back_and_forth=True, trim=False): if variant is None: variant = self.default_variant -- cgit v1.2.3 From aea7b846a2841b587cffe4a31622a79584868caa Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Mon, 7 Jan 2019 19:55:05 -0500 Subject: correct for viewbox --- lib/extensions/lettering.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'lib') diff --git a/lib/extensions/lettering.py b/lib/extensions/lettering.py index fd9c7628..60b4e1f3 100644 --- a/lib/extensions/lettering.py +++ b/lib/extensions/lettering.py @@ -12,6 +12,7 @@ from ..elements import nodes_to_elements from ..gui import PresetsPanel, SimulatorPreview from ..i18n import _ from ..lettering import Font +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 .commands import CommandsExtension @@ -215,7 +216,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): -- cgit v1.2.3 From 4ba3cd708561870a731d9634d9cdd5c18579cac7 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Fri, 22 Feb 2019 22:07:15 -0500 Subject: refactor add_commands() out into commands module --- lib/commands.py | 136 +++++++++++++++++++- lib/extensions/auto_satin.py | 4 +- lib/extensions/base.py | 11 +- lib/extensions/commands.py | 118 ------------------ lib/extensions/layer_commands.py | 4 +- lib/extensions/object_commands.py | 9 +- lib/svg/__init__.py | 7 +- lib/svg/realistic_rendering.py | 130 -------------------- lib/svg/rendering.py | 253 ++++++++++++++++++++++++++++++++++++++ lib/svg/svg.py | 127 ++----------------- 10 files changed, 409 insertions(+), 390 deletions(-) delete mode 100644 lib/svg/realistic_rendering.py create mode 100644 lib/svg/rendering.py (limited to 'lib') diff --git a/lib/commands.py b/lib/commands.py index 3c739708..ddee8326 100644 --- a/lib/commands.py +++ b/lib/commands.py @@ -1,12 +1,18 @@ +from copy import deepcopy +import os +from random import random import sys -import inkex + import cubicsuperpath +import inkex 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 +234,125 @@ 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): + return document.find(SVG_DEFS_TAG) + + +def ensure_symbol(document, command): + """Make sure the command's symbol definition exists in the 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 + + # 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(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..90d8fe33 100644 --- a/lib/extensions/auto_satin.py +++ b/lib/extensions/auto_satin.py @@ -2,6 +2,7 @@ import sys import inkex +from ..commands import add_commands from ..elements import SatinColumn from ..i18n import _ from ..stitches.auto_satin import auto_satin @@ -97,6 +98,5 @@ class AutoSatin(CommandsExtension): 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"]) + add_commands(new_elements[i], ["trim"]) diff --git a/lib/extensions/base.py b/lib/extensions/base.py index 98673541..8d45f790 100644 --- a/lib/extensions/base.py +++ b/lib/extensions/base.py @@ -9,6 +9,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 @@ -194,15 +195,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/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/svg/__init__.py b/lib/svg/__init__.py index df76c0d2..0b4a6ee4 100644 --- a/lib/svg/__init__.py +++ b/lib/svg/__init__.py @@ -1,4 +1,5 @@ -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 .guides import get_guides +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/realistic_rendering.py deleted file mode 100644 index 73da3a09..00000000 --- a/lib/svg/realistic_rendering.py +++ /dev/null @@ -1,130 +0,0 @@ -import simplepath -import math - -from .units import PIXELS_PER_MM -from ..utils import Point - -# The stitch vector path looks like this: -# _______ -# (_______) -# -# It's 0.32mm high, which is the approximate thickness of common machine -# embroidery threads. - -# 1.216 pixels = 0.32mm -stitch_height = 1.216 - -# This vector path starts at the upper right corner of the stitch shape and -# proceeds counter-clockwise.and contains a placeholder (%s) for the stitch -# length. -# -# It contains two invisible "whiskers" of zero width that go above and below -# to ensure that the SVG renderer allocates a large enough canvas area when -# computing the gaussian blur steps. Otherwise, we'd have to expand the -# width and height attributes of the tag to add more buffer space. -# The width and height are specified in multiples of the bounding box -# size, It's the bounding box aligned with the global SVG canvas's axes, not -# the axes of the stitch itself. That means that having a big enough value -# to add enough padding on the long sides of the stitch would waste a ton -# of space on the short sides and significantly slow down rendering. -stitch_path = "M0,0c0.4,0,0.4,0.3,0.4,0.6c0,0.3,-0.1,0.6,-0.4,0.6v0.2,-0.2h-%sc-0.4,0,-0.4,-0.3,-0.4,-0.6c0,-0.3,0.1,-0.6,0.4,-0.6v-0.2,0.2z" - -# This filter makes the above stitch path look like a real stitch with lighting. -realistic_filter = """ - - - - - - - - - - - - - - - - - - -""" - - -def realistic_stitch(start, end): - """Generate a stitch vector path given a start and end point.""" - - end = Point(*end) - start = Point(*start) - - stitch_length = (end - start).length() - stitch_center = (end + start) / 2.0 - stitch_direction = (end - start) - stitch_angle = math.atan2(stitch_direction.y, stitch_direction.x) - - stitch_length = max(0, stitch_length - 0.2 * PIXELS_PER_MM) - - # create the path by filling in the length in the template - path = simplepath.parsePath(stitch_path % stitch_length) - - # rotate the path to match the stitch - rotation_center_x = -stitch_length / 2.0 - rotation_center_y = stitch_height / 2.0 - simplepath.rotatePath(path, stitch_angle, cx=rotation_center_x, cy=rotation_center_y) - - # move the path to the location of the stitch - simplepath.translatePath(path, stitch_center.x - rotation_center_x, stitch_center.y - rotation_center_y) - - return simplepath.formatPath(path) diff --git a/lib/svg/rendering.py b/lib/svg/rendering.py new file mode 100644 index 00000000..41ed53d7 --- /dev/null +++ b/lib/svg/rendering.py @@ -0,0 +1,253 @@ +import math + +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: +# _______ +# (_______) +# +# It's 0.32mm high, which is the approximate thickness of common machine +# embroidery threads. +# 1.216 pixels = 0.32mm +stitch_height = 1.216 + +# This vector path starts at the upper right corner of the stitch shape and +# proceeds counter-clockwise.and contains a placeholder (%s) for the stitch +# length. +# +# It contains two invisible "whiskers" of zero width that go above and below +# to ensure that the SVG renderer allocates a large enough canvas area when +# computing the gaussian blur steps. Otherwise, we'd have to expand the +# width and height attributes of the tag to add more buffer space. +# The width and height are specified in multiples of the bounding box +# size, It's the bounding box aligned with the global SVG canvas's axes, not +# the axes of the stitch itself. That means that having a big enough value +# to add enough padding on the long sides of the stitch would waste a ton +# of space on the short sides and significantly slow down rendering. +stitch_path = "M0,0c0.4,0,0.4,0.3,0.4,0.6c0,0.3,-0.1,0.6,-0.4,0.6v0.2,-0.2h-%sc-0.4,0,-0.4,-0.3,-0.4,-0.6c0,-0.3,0.1,-0.6,0.4,-0.6v-0.2,0.2z" + +# This filter makes the above stitch path look like a real stitch with lighting. +realistic_filter = """ + + + + + + + + + + + + + + + + + + +""" + + +def realistic_stitch(start, end): + """Generate a stitch vector path given a start and end point.""" + + end = Point(*end) + start = Point(*start) + + stitch_length = (end - start).length() + stitch_center = (end + start) / 2.0 + stitch_direction = (end - start) + stitch_angle = math.atan2(stitch_direction.y, stitch_direction.x) + + stitch_length = max(0, stitch_length - 0.2 * PIXELS_PER_MM) + + # create the path by filling in the length in the template + path = simplepath.parsePath(stitch_path % stitch_length) + + # rotate the path to match the stitch + rotation_center_x = -stitch_length / 2.0 + rotation_center_y = stitch_height / 2.0 + simplepath.rotatePath(path, stitch_angle, cx=rotation_center_x, cy=rotation_center_y) + + # move the path to the location of the stitch + 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..0ec43f75 100644 --- a/lib/svg/svg.py +++ b/lib/svg/svg.py @@ -1,124 +1,19 @@ -import inkex -import simplestyle -import simpletransform - -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 = [] - - 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)) +def get_document(node): + return node.getroottree().getroot() - svg.append(layer) - if realistic: - defs = svg.find(SVG_DEFS_TAG) +def generate_unique_id(document, prefix="path"): + 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 -- cgit v1.2.3 From 53a9bd6b31ca3a1f50d41f228e0b598a7d9da8ea Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Mon, 25 Feb 2019 19:49:38 -0500 Subject: add trims in stitches.auto_satin --- lib/extensions/auto_satin.py | 52 ++----------------- lib/stitches/auto_satin.py | 115 ++++++++++++++++++++++++++++++++++++++++--- lib/svg/svg.py | 5 +- 3 files changed, 116 insertions(+), 56 deletions(-) (limited to 'lib') diff --git a/lib/extensions/auto_satin.py b/lib/extensions/auto_satin.py index 90d8fe33..12588d1e 100644 --- a/lib/extensions/auto_satin.py +++ b/lib/extensions/auto_satin.py @@ -38,22 +38,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 @@ -65,38 +49,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: - for i in trim_indices: - 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/stitches/auto_satin.py b/lib/stitches/auto_satin.py index e204a445..aea26427 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): @@ -628,3 +645,87 @@ 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) + + return new_elements diff --git a/lib/svg/svg.py b/lib/svg/svg.py index 0ec43f75..3715def8 100644 --- a/lib/svg/svg.py +++ b/lib/svg/svg.py @@ -1,3 +1,5 @@ +from inkex import etree + from ..utils import cache @@ -6,7 +8,8 @@ def get_document(node): return node.getroottree().getroot() -def generate_unique_id(document, prefix="path"): +def generate_unique_id(document_or_element, prefix="path"): + document = get_document(document_or_element) doc_ids = {node.get('id') for node in document.iterdescendants() if 'id' in node.attrib} i = 1 -- cgit v1.2.3 From 602f201cb6236a7cb4a041b84e761aaedc358ab0 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Wed, 6 Mar 2019 20:32:51 -0500 Subject: implement trim option for lettering --- lib/commands.py | 7 ++++++- lib/extensions/auto_satin.py | 4 ---- lib/extensions/lettering.py | 10 ++++++---- lib/lettering/font.py | 19 +++++++++---------- 4 files changed, 21 insertions(+), 19 deletions(-) (limited to 'lib') diff --git a/lib/commands.py b/lib/commands.py index ddee8326..53b9e77f 100644 --- a/lib/commands.py +++ b/lib/commands.py @@ -254,7 +254,12 @@ def symbol_defs(): @cache def get_defs(document): - return document.find(SVG_DEFS_TAG) + 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): diff --git a/lib/extensions/auto_satin.py b/lib/extensions/auto_satin.py index 12588d1e..b7cee83b 100644 --- a/lib/extensions/auto_satin.py +++ b/lib/extensions/auto_satin.py @@ -2,12 +2,8 @@ import sys import inkex -from ..commands import add_commands -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 diff --git a/lib/extensions/lettering.py b/lib/extensions/lettering.py index 60b4e1f3..3718b5ab 100644 --- a/lib/extensions/lettering.py +++ b/lib/extensions/lettering.py @@ -37,11 +37,11 @@ class LetteringFrame(wx.Frame): 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.SetValue(bool(self.settings.trim)) - self.Bind(wx.EVT_CHECKBOX, lambda event: self.on_change("trim", event)) + 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")) @@ -94,7 +94,8 @@ class LetteringFrame(wx.Frame): def update_lettering(self): font_path = os.path.join(get_bundled_dir("fonts"), self.settings.font) font = Font(font_path) - self.group[:] = font.render_text(self.settings.text, back_and_forth=self.settings.back_and_forth, trim=self.settings.trim) + del self.group[:] + font.render_text(self.settings.text, self.group, back_and_forth=self.settings.back_and_forth, trim=self.settings.trim) def generate_patches(self, abort_early=None): patches = [] @@ -155,7 +156,8 @@ 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) text_editor_sizer = wx.StaticBoxSizer(self.text_editor_box, wx.VERTICAL) diff --git a/lib/lettering/font.py b/lib/lettering/font.py index 05465c8c..28807cd6 100644 --- a/lib/lettering/font.py +++ b/lib/lettering/font.py @@ -81,7 +81,9 @@ class Font(object): kerning_pairs = font_metadata('kerning_pairs', {}) auto_satin = font_metadata('auto_satin', True) - def render_text(self, text, variant=None, back_and_forth=True, trim=False): + def render_text(self, text, destination_group, variant=None, back_and_forth=True, trim=False): + """Render text into an SVG group element.""" + if variant is None: variant = self.default_variant @@ -90,9 +92,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 +100,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 +173,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 +181,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) -- cgit v1.2.3 From a14ed903cf6043e4138d7d3d59a5cac9750cb191 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Fri, 8 Mar 2019 19:59:28 -0500 Subject: auto satin should trim at the end too --- lib/stitches/auto_satin.py | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'lib') diff --git a/lib/stitches/auto_satin.py b/lib/stitches/auto_satin.py index aea26427..7c09b023 100644 --- a/lib/stitches/auto_satin.py +++ b/lib/stitches/auto_satin.py @@ -728,4 +728,8 @@ def add_trims(elements, trim_indices): new_elements.append(element) + # trim at the end, too + if i not in trim_indices: + add_commands(element, ["trim"]) + return new_elements -- cgit v1.2.3 From fb3c8186d275afa18c8146a453654afaf879ed34 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Fri, 8 Mar 2019 20:06:36 -0500 Subject: lower trim threshold to 0.75mm --- lib/stitches/auto_satin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib') diff --git a/lib/stitches/auto_satin.py b/lib/stitches/auto_satin.py index 7c09b023..75b13176 100644 --- a/lib/stitches/auto_satin.py +++ b/lib/stitches/auto_satin.py @@ -613,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 -- cgit v1.2.3 From d1c001857d1e389950b3f346c1a0413d82899ca4 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Fri, 8 Mar 2019 20:50:48 -0500 Subject: get root properly --- lib/svg/svg.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'lib') diff --git a/lib/svg/svg.py b/lib/svg/svg.py index 3715def8..464a2a18 100644 --- a/lib/svg/svg.py +++ b/lib/svg/svg.py @@ -9,7 +9,11 @@ def get_document(node): def generate_unique_id(document_or_element, prefix="path"): - document = get_document(document_or_element) + if isinstance(document_or_element, etree._ElementTree): + document = document_or_element.getroot() + else: + document = get_document(document_or_element) + doc_ids = {node.get('id') for node in document.iterdescendants() if 'id' in node.attrib} i = 1 -- cgit v1.2.3 From 13b6c67644acaeff04918e8489e943b4a8795863 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Fri, 8 Mar 2019 20:51:23 -0500 Subject: less haphazard positioning for commands --- lib/commands.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) (limited to 'lib') diff --git a/lib/commands.py b/lib/commands.py index 53b9e77f..8e35d7ee 100644 --- a/lib/commands.py +++ b/lib/commands.py @@ -5,6 +5,7 @@ import sys import cubicsuperpath import inkex +from shapely import geometry as shgeo import simpletransform from .i18n import _, N_ @@ -325,10 +326,15 @@ def get_command_pos(element, index, total): # 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 + # 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.1 + position += random() * 0.05 + position += start_position return outline.interpolate(position, normalized=True) -- cgit v1.2.3 From 55505369496c0986e54fe5722e7e8ddce0a9294e Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Mon, 25 Mar 2019 19:40:37 -0400 Subject: implement font chooser --- lib/extensions/lettering.py | 69 ++++++++++++++++++++++++++++++++++++++------- lib/lettering/__init__.py | 2 +- lib/lettering/font.py | 12 ++++++++ 3 files changed, 72 insertions(+), 11 deletions(-) (limited to 'lib') diff --git a/lib/extensions/lettering.py b/lib/extensions/lettering.py index 3718b5ab..a33277df 100644 --- a/lib/extensions/lettering.py +++ b/lib/extensions/lettering.py @@ -5,20 +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 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') @@ -46,8 +49,9 @@ class LetteringFrame(wx.Frame): # text editor self.text_editor_box = wx.StaticBox(self, wx.ID_ANY, label=_("Text")) - self.font_chooser = wx.ComboBox(self, wx.ID_ANY) + self.font_chooser = wx.ComboBox(self, wx.ID_ANY, style=wx.CB_READONLY) self.update_font_list() + self.set_initial_font(self.settings.font) 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)) @@ -73,7 +77,7 @@ class LetteringFrame(wx.Frame): self.settings = DotDict({ "text": u"", "back_and_forth": True, - "font": "small_font" + "font": None }) def save_settings(self): @@ -87,13 +91,61 @@ 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 + + self.font_chooser.SetItems(sorted(self.fonts)) + + if len(self.fonts) == 0: + info_dialog(self, _("Unable to find any fonts! Please try reinstalling Ink/Stitch.")) + self.cancel() + + def set_initial_font(self, font_id): + if font_id is not None: + if font_id not in self.fonts_by_id: + info_dialog(self, _( + '''This text was created using the font "%s", but Ink/Stitch can't find that font. A default font will be substituted.''') % font_id) + + try: + self.font_chooser.SetValue(self.fonts_by_id[font_id].name) + except KeyError: + self.font_chooser.SetValue(self.default_font.name) + + @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 update_lettering(self): - font_path = os.path.join(get_bundled_dir("fonts"), self.settings.font) - font = Font(font_path) + font = self.fonts_by_id.get(self.settings.font, self.default_font) del self.group[:] font.render_text(self.settings.text, self.group, back_and_forth=self.settings.back_and_forth, trim=self.settings.trim) @@ -120,9 +172,6 @@ class LetteringFrame(wx.Frame): return patches - def update_font_list(self): - pass - def get_preset_data(self): # called by self.presets_panel preset = {} 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 28807cd6..6f749d28 100644 --- a/lib/lettering/font.py +++ b/lib/lettering/font.py @@ -7,6 +7,7 @@ import os import inkex from ..elements import nodes_to_elements +from ..exceptions import InkstitchException from ..i18n import _ from ..stitches.auto_satin import auto_satin from ..svg import PIXELS_PER_MM @@ -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) @@ -47,6 +52,9 @@ class Font(object): self._load_license() self._load_variants() + if self.variants.get(self.default_variant) is None: + raise FontError("font not found or has no default variant") + def _load_metadata(self): try: with open(os.path.join(self.path, "font.json")) as metadata_file: @@ -81,6 +89,10 @@ class Font(object): kerning_pairs = font_metadata('kerning_pairs', {}) auto_satin = font_metadata('auto_satin', True) + @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.""" -- cgit v1.2.3 From a9cf553066c3fd5b907593751bb00f77f32ce86a Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Tue, 2 Apr 2019 22:36:54 -0400 Subject: add font description to font selector dropdown --- lib/extensions/lettering.py | 31 ++++++++++++---- lib/gui/__init__.py | 1 + lib/gui/subtitle_combo_box.py | 85 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 109 insertions(+), 8 deletions(-) create mode 100644 lib/gui/subtitle_combo_box.py (limited to 'lib') diff --git a/lib/extensions/lettering.py b/lib/extensions/lettering.py index a33277df..74d036cf 100644 --- a/lib/extensions/lettering.py +++ b/lib/extensions/lettering.py @@ -10,7 +10,7 @@ import inkex import wx from ..elements import nodes_to_elements -from ..gui import PresetsPanel, SimulatorPreview, info_dialog +from ..gui import PresetsPanel, SimulatorPreview, info_dialog, SubtitleComboBox from ..i18n import _ from ..lettering import Font, FontError from ..svg import get_correction_transform @@ -49,8 +49,10 @@ class LetteringFrame(wx.Frame): # text editor self.text_editor_box = wx.StaticBox(self, wx.ID_ANY, label=_("Text")) - self.font_chooser = wx.ComboBox(self, wx.ID_ANY, style=wx.CB_READONLY) 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.update_preview) self.set_initial_font(self.settings.font) self.text_editor = wx.TextCtrl(self, style=wx.TE_MULTILINE | wx.TE_DONTWRAP, value=self.settings.text) @@ -115,12 +117,19 @@ class LetteringFrame(wx.Frame): except FontError: pass - self.font_chooser.SetItems(sorted(self.fonts)) - 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 is not None: if font_id not in self.fonts_by_id: @@ -128,9 +137,9 @@ class LetteringFrame(wx.Frame): '''This text was created using the font "%s", but Ink/Stitch can't find that font. A default font will be substituted.''') % font_id) try: - self.font_chooser.SetValue(self.fonts_by_id[font_id].name) + self.font_chooser.SetValueByUser(self.fonts_by_id[font_id].name) except KeyError: - self.font_chooser.SetValue(self.default_font.name) + self.font_chooser.SetValueByUser(self.default_font.name) @property @cache @@ -144,8 +153,11 @@ class LetteringFrame(wx.Frame): self.settings[attribute] = event.GetEventObject().GetValue() self.preview.update() + def update_preview(self, event=None): + self.preview.update() + def update_lettering(self): - font = self.fonts_by_id.get(self.settings.font, self.default_font) + font = self.fonts.get(self.font_chooser.GetValue(), self.default_font) del self.group[:] font.render_text(self.settings.text, self.group, back_and_forth=self.settings.back_and_forth, trim=self.settings.trim) @@ -209,8 +221,11 @@ class LetteringFrame(wx.Frame): 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_chooser_sizer = wx.BoxSizer(wx.VERTICAL) + font_chooser_sizer.Add(self.font_chooser, 0, wx.ALL | wx.EXPAND, 10) + 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_chooser_sizer, 0, wx.RIGHT | wx.EXPAND, 100) 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) diff --git a/lib/gui/__init__.py b/lib/gui/__init__.py index 060c3d93..51890cf9 100644 --- a/lib/gui/__init__.py +++ b/lib/gui/__init__.py @@ -1,3 +1,4 @@ from dialogs import info_dialog, confirm_dialog from presets import PresetsPanel from simulator import EmbroiderySimulator, SimulatorPreview, show_simulator +from subtitle_combo_box import SubtitleComboBox 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) -- cgit v1.2.3 From a6a86973dd54d623394fdbfc61e6e4c0ca263cc0 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Tue, 2 Apr 2019 23:07:38 -0400 Subject: add localization for font names and descriptions --- lib/i18n.py | 28 ++++++++++++++++++++++++++-- lib/lettering/font.py | 25 ++++++++++++++++++++++--- 2 files changed, 48 insertions(+), 5 deletions(-) (limited to 'lib') 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/font.py b/lib/lettering/font.py index 6f749d28..46e2648d 100644 --- a/lib/lettering/font.py +++ b/lib/lettering/font.py @@ -8,7 +8,7 @@ import inkex from ..elements import nodes_to_elements from ..exceptions import InkstitchException -from ..i18n import _ +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 @@ -32,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. @@ -79,8 +98,8 @@ class Font(object): # we'll deal with missing variants when we apply lettering pass - name = font_metadata('name', '') - description = font_metadata('description', '') + 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) -- cgit v1.2.3 From 6c5e57d39c9a72a9a56acc3b7ffe93e67f38ecb1 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Tue, 2 Apr 2019 23:39:54 -0400 Subject: implement lettering presets --- lib/extensions/lettering.py | 38 ++++++++++++++++++++++++-------------- lib/gui/presets.py | 11 ++++++----- 2 files changed, 30 insertions(+), 19 deletions(-) (limited to 'lib') diff --git a/lib/extensions/lettering.py b/lib/extensions/lettering.py index 74d036cf..9193a723 100644 --- a/lib/extensions/lettering.py +++ b/lib/extensions/lettering.py @@ -33,17 +33,13 @@ 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.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.SetValue(bool(self.settings.trim)) self.trim_checkbox.Bind(wx.EVT_CHECKBOX, lambda event: self.on_change("trim", event)) # text editor @@ -53,10 +49,9 @@ class LetteringFrame(wx.Frame): 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.update_preview) - self.set_initial_font(self.settings.font) - 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) @@ -66,9 +61,12 @@ 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""" try: if INKSTITCH_LETTERING in self.group.attrib: self.settings = DotDict(json.loads(b64decode(self.group.get(INKSTITCH_LETTERING)))) @@ -82,7 +80,16 @@ class LetteringFrame(wx.Frame): "font": None }) + def apply_settings(self): + """Make the settings in self.settings visible in the UI.""" + self.back_and_forth_checkbox.SetValue(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) + 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 @@ -186,12 +193,15 @@ class LetteringFrame(wx.Frame): def get_preset_data(self): # called by self.presets_panel - preset = {} - return preset - - def apply_preset_data(self): - # called by self.presets_panel - return + settings = dict(self.settings) + del settings["text"] + return settings + + 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 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() -- cgit v1.2.3 From 98e59f255039911a4c1cc009325f7b30839cafdd Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Wed, 10 Apr 2019 20:23:11 -0400 Subject: add support for scaling text --- lib/extensions/lettering.py | 32 ++++++++++++++++++++++++++------ lib/lettering/font.py | 2 ++ 2 files changed, 28 insertions(+), 6 deletions(-) (limited to 'lib') diff --git a/lib/extensions/lettering.py b/lib/extensions/lettering.py index 9193a723..428bb0ca 100644 --- a/lib/extensions/lettering.py +++ b/lib/extensions/lettering.py @@ -48,7 +48,10 @@ class LetteringFrame(wx.Frame): 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.update_preview) + 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) self.text_editor.Bind(wx.EVT_TEXT, lambda event: self.on_change("text", event)) @@ -77,7 +80,8 @@ class LetteringFrame(wx.Frame): self.settings = DotDict({ "text": u"", "back_and_forth": True, - "font": None + "font": None, + "scale": 100 }) def apply_settings(self): @@ -86,6 +90,7 @@ class LetteringFrame(wx.Frame): 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.""" @@ -148,6 +153,8 @@ class LetteringFrame(wx.Frame): except KeyError: self.font_chooser.SetValueByUser(self.default_font.name) + self.on_font_changed() + @property @cache def default_font(self): @@ -160,14 +167,24 @@ class LetteringFrame(wx.Frame): 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.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): - font = self.fonts.get(self.font_chooser.GetValue(), self.default_font) del self.group[:] + self.group.attrib.pop('transform', None) + + font = self.fonts.get(self.font_chooser.GetValue(), self.default_font) font.render_text(self.settings.text, self.group, back_and_forth=self.settings.back_and_forth, trim=self.settings.trim) + if self.settings.scale != 100: + self.group.attrib['transform'] = 'scale(%s)' % (self.settings.scale / 100.0) + def generate_patches(self, abort_early=None): patches = [] @@ -231,11 +248,14 @@ class LetteringFrame(wx.Frame): 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_chooser_sizer = wx.BoxSizer(wx.VERTICAL) - font_chooser_sizer.Add(self.font_chooser, 0, wx.ALL | wx.EXPAND, 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(font_chooser_sizer, 0, wx.RIGHT | wx.EXPAND, 100) + 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) diff --git a/lib/lettering/font.py b/lib/lettering/font.py index 46e2648d..883821ec 100644 --- a/lib/lettering/font.py +++ b/lib/lettering/font.py @@ -107,6 +107,8 @@ 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): -- cgit v1.2.3 From 313cd44483bf216c123e19dfb3dd294eb57a9c3d Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Mon, 15 Apr 2019 20:26:30 -0400 Subject: don't overwrite user's positioning of text when re-editing --- lib/extensions/lettering.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) (limited to 'lib') diff --git a/lib/extensions/lettering.py b/lib/extensions/lettering.py index 428bb0ca..e6b828a8 100644 --- a/lib/extensions/lettering.py +++ b/lib/extensions/lettering.py @@ -177,13 +177,22 @@ class LetteringFrame(wx.Frame): def update_lettering(self): del self.group[:] - self.group.attrib.pop('transform', None) + + 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, self.group, back_and_forth=self.settings.back_and_forth, trim=self.settings.trim) + 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: - self.group.attrib['transform'] = 'scale(%s)' % (self.settings.scale / 100.0) + destination_group.attrib['transform'] = 'scale(%s)' % (self.settings.scale / 100.0) def generate_patches(self, abort_early=None): patches = [] -- cgit v1.2.3 From 30d80ab41bfe19b5bd9d71a903c5e796266a849b Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Tue, 16 Apr 2019 21:01:25 -0400 Subject: add scale bar to simulator for comparison --- lib/gui/simulator.py | 55 +++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 46 insertions(+), 9 deletions(-) (limited to 'lib') diff --git a/lib/gui/simulator.py b/lib/gui/simulator.py index e0d78983..c07a7af3 100644 --- a/lib/gui/simulator.py +++ b/lib/gui/simulator.py @@ -356,14 +356,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 clear(self): dc = wx.ClientDC(self) @@ -520,7 +557,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 -- cgit v1.2.3 From fee43e0941b11c709d7ae23fcd4bee4cd89d2a55 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Thu, 18 Apr 2019 11:35:29 -0400 Subject: fix parameter management --- lib/extensions/lettering.py | 18 ++++++++++-------- lib/utils/dotdict.py | 6 ++++++ 2 files changed, 16 insertions(+), 8 deletions(-) (limited to 'lib') diff --git a/lib/extensions/lettering.py b/lib/extensions/lettering.py index e6b828a8..17435492 100644 --- a/lib/extensions/lettering.py +++ b/lib/extensions/lettering.py @@ -70,12 +70,6 @@ class LetteringFrame(wx.Frame): def load_settings(self): """Load the settings saved into the SVG group element""" - try: - if INKSTITCH_LETTERING in self.group.attrib: - self.settings = DotDict(json.loads(b64decode(self.group.get(INKSTITCH_LETTERING)))) - return - except (TypeError, ValueError): - pass self.settings = DotDict({ "text": u"", @@ -84,9 +78,16 @@ class LetteringFrame(wx.Frame): "scale": 100 }) + try: + if INKSTITCH_LETTERING in self.group.attrib: + self.settings.update(json.loads(b64decode(self.group.get(INKSTITCH_LETTERING)))) + return + except (TypeError, ValueError): + pass + def apply_settings(self): """Make the settings in self.settings visible in the UI.""" - self.back_and_forth_checkbox.SetValue(self.settings.back_and_forth) + 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) @@ -143,7 +144,7 @@ class LetteringFrame(wx.Frame): return {font.name: font.description for font in self.fonts.itervalues()} def set_initial_font(self, font_id): - if font_id is not None: + if font_id: if font_id not in self.fonts_by_id: info_dialog(self, _( '''This text was created using the font "%s", but Ink/Stitch can't find that font. A default font will be substituted.''') % font_id) @@ -169,6 +170,7 @@ class LetteringFrame(wx.Frame): 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() 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) -- cgit v1.2.3 From b307b8e8247678a4bf128ded80a9bfd7b9b54c81 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Sat, 20 Apr 2019 22:01:58 -0400 Subject: fix style --- lib/extensions/lettering.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'lib') diff --git a/lib/extensions/lettering.py b/lib/extensions/lettering.py index 17435492..a2a729b5 100644 --- a/lib/extensions/lettering.py +++ b/lib/extensions/lettering.py @@ -146,8 +146,9 @@ class LetteringFrame(wx.Frame): def set_initial_font(self, font_id): if font_id: if font_id not in self.fonts_by_id: - info_dialog(self, _( - '''This text was created using the font "%s", but Ink/Stitch can't find that font. A default font will be substituted.''') % font_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) -- cgit v1.2.3 From 013b4c2739edaa4336132fbc87eb131b38c7eb54 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Tue, 30 Apr 2019 20:15:58 -0400 Subject: speed up startup by lazy-loading glyphs --- lib/lettering/font.py | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) (limited to 'lib') diff --git a/lib/lettering/font.py b/lib/lettering/font.py index 883821ec..13a2b78c 100644 --- a/lib/lettering/font.py +++ b/lib/lettering/font.py @@ -67,36 +67,39 @@ 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() - - if self.variants.get(self.default_variant) is None: - raise FontError("font not found or has no default variant") 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 + 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', '') @@ -116,6 +119,7 @@ class Font(object): 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() if variant is None: variant = self.default_variant -- cgit v1.2.3