diff options
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/commands.py | 4 | ||||
| -rw-r--r-- | lib/elements/fill_stitch.py | 7 | ||||
| -rw-r--r-- | lib/elements/satin_column.py | 8 | ||||
| -rw-r--r-- | lib/elements/stroke.py | 12 | ||||
| -rw-r--r-- | lib/elements/utils.py | 2 | ||||
| -rw-r--r-- | lib/extensions/__init__.py | 2 | ||||
| -rw-r--r-- | lib/extensions/auto_satin.py | 9 | ||||
| -rw-r--r-- | lib/extensions/fill_to_stroke.py | 2 | ||||
| -rw-r--r-- | lib/extensions/lettering.py | 88 | ||||
| -rw-r--r-- | lib/extensions/lettering_generate_json.py | 36 | ||||
| -rw-r--r-- | lib/extensions/lettering_update_json_glyphlist.py | 41 | ||||
| -rwxr-xr-x | lib/inx/extensions.py | 2 | ||||
| -rw-r--r-- | lib/lettering/categories.py | 30 | ||||
| -rw-r--r-- | lib/lettering/font.py | 2 | ||||
| -rw-r--r-- | lib/lettering/font_info.py (renamed from lib/lettering/kerning.py) | 14 | ||||
| -rw-r--r-- | lib/stitches/auto_fill.py | 30 | ||||
| -rw-r--r-- | lib/stitches/circular_fill.py | 6 | ||||
| -rw-r--r-- | lib/stitches/contour_fill.py | 6 | ||||
| -rw-r--r-- | lib/stitches/guided_fill.py | 10 | ||||
| -rw-r--r-- | lib/stitches/running_stitch.py | 4 | ||||
| -rw-r--r-- | lib/threads/color.py | 19 | ||||
| -rw-r--r-- | lib/utils/clamp_path.py | 21 | ||||
| -rw-r--r-- | lib/utils/smoothing.py | 10 |
23 files changed, 281 insertions, 84 deletions
diff --git a/lib/commands.py b/lib/commands.py index bd4bd6d5..d93954ec 100644 --- a/lib/commands.py +++ b/lib/commands.py @@ -88,6 +88,10 @@ class BaseCommand(object): if self.command.startswith('inkstitch_'): self.command = self.command[10:] + # It is possible that through copy paste or whatever user action a command is defined multiple times + # in the defs section. In this case the id will be altered with an additional number (e.g. inkstitch_trim-5) + # Let's make sure to remove the number part to recognize the command correctly + self.command = self.command.split("-")[0] else: raise CommandParseError("symbol is not an Ink/Stitch command") diff --git a/lib/elements/fill_stitch.py b/lib/elements/fill_stitch.py index 80e68247..b93d7ff5 100644 --- a/lib/elements/fill_stitch.py +++ b/lib/elements/fill_stitch.py @@ -364,12 +364,11 @@ class FillStitch(EmbroideryElement): @property @param('running_stitch_tolerance_mm', _('Running stitch tolerance'), - tooltip=_('All stitches must be within this distance of the path. ' + - 'A lower tolerance means stitches will be closer together. ' + - 'A higher tolerance means sharp corners may be rounded.'), + tooltip=_('Determines how hard Ink/Stitch tries to avoid stitching outside the shape.' + + 'Lower numbers are less likely to stitch outside the shape but require more stitches.'), unit='mm', type='float', - default=0.2, + default=0.1, sort_index=32) def running_stitch_tolerance(self): return max(self.get_float_param("running_stitch_tolerance_mm", 0.2), 0.01) diff --git a/lib/elements/satin_column.py b/lib/elements/satin_column.py index dc883dea..86c6d05a 100644 --- a/lib/elements/satin_column.py +++ b/lib/elements/satin_column.py @@ -830,6 +830,8 @@ class SatinColumn(EmbroideryElement): def center_line(self): # similar technique to do_center_walk() center_walk = [p[0] for p in self.plot_points_on_rails(self.zigzag_spacing, (0, 0), (-0.5, -0.5))] + if len(center_walk) < 2: + center_walk = [center_walk[0], center_walk[0]] return shgeo.LineString(center_walk) def offset_points(self, pos1, pos2, offset_px, offset_proportional): @@ -907,7 +909,7 @@ class SatinColumn(EmbroideryElement): # Base the number of stitches in each section on the _longer_ of # the two sections. Otherwise, things could get too sparse when one # side is significantly longer (e.g. when going around a corner). - num_points = max(path0.length, path1.length) / spacing + num_points = max(path0.length, path1.length, 0.01) / spacing # Section stitch spacing and the cursor are expressed as a fraction # of the total length of the path, because we use normalized=True @@ -965,7 +967,7 @@ class SatinColumn(EmbroideryElement): # more than 5%. if iterations <= 2: distance = self._stitch_distance(pos0, pos1, old_pos0, old_pos1) - if abs((current_spacing - distance) / current_spacing) > 0.05: + if distance > 0.01 and abs((current_spacing - distance) / current_spacing) > 0.05: # We'll revise to_travel then go back to the start of # the loop and try again. to_travel = (current_spacing / distance) * to_travel @@ -1294,6 +1296,6 @@ class SatinProcessor: if self.use_random: roll = prng.uniform_floats(self.seed, self.cycle) self.cycle += 1 - return 1.0 + ((roll[0] - 0.5) * 2) * self.random_zigzag_spacing + return max(1.0 + ((roll[0] - 0.5) * 2) * self.random_zigzag_spacing, 0.01) else: return 1.0 diff --git a/lib/elements/stroke.py b/lib/elements/stroke.py index ac54908b..d845ed01 100644 --- a/lib/elements/stroke.py +++ b/lib/elements/stroke.py @@ -31,6 +31,15 @@ class MultipleGuideLineWarning(ValidationWarning): ] +class TooFewSubpathsWarning(ValidationWarning): + name = _("Too few subpaths") + description = _("This element renders as running stitch while it has a satin column parameter.") + steps_to_solve = [ + _("* Convert to stroke: select the element and open the parameter dialog. Enable running stitch along path."), + _("* Use as satin column: add an other rail and optionally rungs.") + ] + + class Stroke(EmbroideryElement): element_name = _("Stroke") @@ -567,6 +576,9 @@ class Stroke(EmbroideryElement): return coords[int(len(coords)/2)] def validation_warnings(self): + # satin column warning + if self.get_boolean_param("satin_column", False): + yield TooFewSubpathsWarning(self._representative_point()) # guided fill warnings if self.stroke_method == 1: guide_lines = get_marker_elements(self.node, "guide-line", False, True, True) diff --git a/lib/elements/utils.py b/lib/elements/utils.py index f7ee8dbc..d5122e3f 100644 --- a/lib/elements/utils.py +++ b/lib/elements/utils.py @@ -41,7 +41,7 @@ def node_to_elements(node, clone_to_element=False): # noqa: C901 if element.get_style("fill", "black") and not element.get_style('fill-opacity', 1) == "0": elements.append(FillStitch(node)) if element.get_style("stroke"): - if element.get_boolean_param("satin_column"): + if element.get_boolean_param("satin_column") and len(element.path) > 1: elements.append(SatinColumn(node)) elif not is_command(element.node): elements.append(Stroke(node)) diff --git a/lib/extensions/__init__.py b/lib/extensions/__init__.py index 25d3214c..d0900f2d 100644 --- a/lib/extensions/__init__.py +++ b/lib/extensions/__init__.py @@ -32,6 +32,7 @@ from .lettering_custom_font_dir import LetteringCustomFontDir from .lettering_force_lock_stitches import LetteringForceLockStitches from .lettering_generate_json import LetteringGenerateJson from .lettering_remove_kerning import LetteringRemoveKerning +from .lettering_update_json_glyphlist import LetteringUpdateJsonGlyphlist from .letters_to_font import LettersToFont from .object_commands import ObjectCommands from .object_commands_toggle_visibility import ObjectCommandsToggleVisibility @@ -84,6 +85,7 @@ __all__ = extensions = [StitchPlanPreview, AutoRun, Lettering, LetteringGenerateJson, + LetteringUpdateJsonGlyphlist, LetteringRemoveKerning, LetteringCustomFontDir, LetteringForceLockStitches, diff --git a/lib/extensions/auto_satin.py b/lib/extensions/auto_satin.py index dfb1a87e..badc9d55 100644 --- a/lib/extensions/auto_satin.py +++ b/lib/extensions/auto_satin.py @@ -63,7 +63,12 @@ class AutoSatin(CommandsExtension): starting_point = self.get_starting_point() ending_point = self.get_ending_point() - # Ignore fills - elements = [element for element in self.elements if isinstance(element, SatinColumn) or isinstance(element, Stroke)] + # Ignore fills and zero length satins + elements = [element for element in self.elements if (isinstance(element, SatinColumn) and element.center_line.length != 0) or + isinstance(element, Stroke)] + + # at this point we possibly removed all the elements, in this case stop here + if not elements: + return auto_satin(elements, self.options.preserve_order, starting_point, ending_point, self.options.trim) diff --git a/lib/extensions/fill_to_stroke.py b/lib/extensions/fill_to_stroke.py index 5672fb69..28c1f651 100644 --- a/lib/extensions/fill_to_stroke.py +++ b/lib/extensions/fill_to_stroke.py @@ -104,7 +104,7 @@ class FillToStroke(InkstitchExtension): pass # remove empty groups for parent in set(parents): - if not parent.getchildren(): + if parent is not None and not parent.getchildren(): parent.getparent().remove(parent) def _get_high_res_polygon(self, polygon): diff --git a/lib/extensions/lettering.py b/lib/extensions/lettering.py index fec48100..0879c30f 100644 --- a/lib/extensions/lettering.py +++ b/lib/extensions/lettering.py @@ -18,13 +18,14 @@ from ..elements import nodes_to_elements from ..gui import PresetsPanel, SimulatorPreview, info_dialog from ..i18n import _ from ..lettering import Font, FontError +from ..lettering.categories import FONT_CATEGORIES, FontCategory from ..svg import get_correction_transform from ..svg.tags import (INKSCAPE_LABEL, INKSTITCH_LETTERING, SVG_GROUP_TAG, SVG_PATH_TAG) from ..utils import DotDict, cache, get_bundled_dir, get_resource_dir +from ..utils.threading import ExitThread from .commands import CommandsExtension from .lettering_custom_font_dir import get_custom_font_dir -from ..utils.threading import ExitThread class LetteringFrame(wx.Frame): @@ -52,19 +53,32 @@ class LetteringFrame(wx.Frame): self.font_chooser = wx.adv.BitmapComboBox(self, wx.ID_ANY, style=wx.CB_READONLY | wx.CB_SORT) self.font_chooser.Bind(wx.EVT_COMBOBOX, self.on_font_changed) - self.font_filter = fs.FloatSpin(self, min_val=0, max_val=None, increment=1, value="0") - self.font_filter.SetFormat("%f") - self.font_filter.SetDigits(2) - self.font_filter.Bind(fs.EVT_FLOATSPIN, self.on_filter_changed) - self.font_filter.SetToolTip(_("Font size filter (mm). 0 for all sizes.")) - - self.update_font_list() - self.set_font_list() + self.font_size_filter = fs.FloatSpin(self, min_val=0, max_val=None, increment=1, value="0") + self.font_size_filter.SetFormat("%f") + self.font_size_filter.SetDigits(2) + self.font_size_filter.Bind(fs.EVT_FLOATSPIN, self.on_filter_changed) + self.font_size_filter.SetToolTip(_("Font size filter (mm). 0 for all sizes.")) + + self.font_glyph_filter = wx.CheckBox(self, label=_("Glyphs")) + self.font_glyph_filter.Bind(wx.EVT_CHECKBOX, self.on_filter_changed) + self.font_glyph_filter.SetToolTip(_("Filter fonts by available glyphs.")) + + self.font_category_filter = wx.ComboBox(self, wx.ID_ANY, choices=[], style=wx.CB_DROPDOWN | wx.CB_READONLY) + unfiltered = FontCategory('unfiltered', "---") + self.font_category_filter.Append(unfiltered.name, unfiltered) + for category in FONT_CATEGORIES: + self.font_category_filter.Append(category.name, category) + self.font_category_filter.SetToolTip(_("Filter fonts by category.")) + self.font_category_filter.SetSelection(0) + self.font_category_filter.Bind(wx.EVT_COMBOBOX, self.on_filter_changed) # font details self.font_description = wx.StaticText(self, wx.ID_ANY) self.Bind(wx.EVT_SIZE, self.resize) + # font filter + self.filter_box = wx.StaticBox(self, wx.ID_ANY, label=_("Font Filter")) + # options self.options_box = wx.StaticBox(self, wx.ID_ANY, label=_("Options")) @@ -95,6 +109,10 @@ class LetteringFrame(wx.Frame): self.apply_button = wx.Button(self, wx.ID_ANY, _("Apply and Quit")) self.apply_button.Bind(wx.EVT_BUTTON, self.apply) + # set font list + self.update_font_list() + self.set_font_list() + self.__do_layout() self.load_settings() @@ -165,10 +183,26 @@ class LetteringFrame(wx.Frame): self.fonts = {} self.fonts_by_id = {} - filter_size = self.font_filter.GetValue() + # font size filter value + filter_size = self.font_size_filter.GetValue() + filter_glyph = self.font_glyph_filter.GetValue() + filter_category = self.font_category_filter.GetSelection() - 1 + + # glyph filter string without spaces + glyphs = [*self.text_editor.GetValue().replace(" ", "").replace("\n", "")] + for font in self.font_list: + if filter_glyph and glyphs and not set(glyphs).issubset(font.available_glyphs): + continue + + if filter_category != -1: + category = FONT_CATEGORIES[filter_category].id + if category not in font.keywords: + continue + if filter_size != 0 and (filter_size < font.size * font.min_scale or filter_size > font.size * font.max_scale): continue + self.fonts[font.marked_custom_font_name] = font self.fonts_by_id[font.marked_custom_font_id] = font @@ -208,7 +242,7 @@ class LetteringFrame(wx.Frame): try: font = self.fonts_by_id[font_id].marked_custom_font_name except KeyError: - font = self.default_font.name + font = self.default_font.marked_custom_font_name self.font_chooser.SetValue(font) self.on_font_changed() @@ -222,6 +256,8 @@ class LetteringFrame(wx.Frame): def on_change(self, attribute, event): self.settings[attribute] = event.GetEventObject().GetValue() + if attribute == "text" and self.font_glyph_filter.GetValue() is True: + self.on_filter_changed() self.preview.update() def on_trim_option_change(self, event=None): @@ -232,7 +268,7 @@ class LetteringFrame(wx.Frame): font = self.fonts.get(self.font_chooser.GetValue(), self.default_font) self.settings.font = font.marked_custom_font_id - filter_size = self.font_filter.GetValue() + filter_size = self.font_size_filter.GetValue() self.scale_spinner.SetRange(int(font.min_scale * 100), int(font.max_scale * 100)) if filter_size != 0: self.scale_spinner.SetValue(int(filter_size / font.size * 100)) @@ -245,7 +281,7 @@ class LetteringFrame(wx.Frame): pass # Update font description - color = (0, 0, 0) + color = wx.NullColour description = font.description if len(font_variants) == 0: color = (255, 0, 0) @@ -271,17 +307,17 @@ class LetteringFrame(wx.Frame): if not self.fonts: # No fonts for filtered size self.font_chooser.Clear() - self.filter_label.SetForegroundColour("red") + self.filter_box.SetForegroundColour("red") return else: - self.filter_label.SetForegroundColour("black") + self.filter_box.SetForegroundColour(wx.NullColour) - filter_size = self.font_filter.GetValue() + filter_size = self.font_size_filter.GetValue() previous_font = self.font_chooser.GetValue() self.set_font_list() font = self.fonts.get(previous_font, self.default_font) - self.font_chooser.SetValue(font.name) - if font.name != previous_font: + self.font_chooser.SetValue(font.marked_custom_font_name) + if font.marked_custom_font_name != previous_font: self.on_font_changed() elif filter_size != 0: self.scale_spinner.SetValue(int(filter_size / font.size * 100)) @@ -396,19 +432,27 @@ class LetteringFrame(wx.Frame): font_selector_sizer = wx.StaticBoxSizer(self.font_selector_box, wx.VERTICAL) font_selector_box = wx.BoxSizer(wx.HORIZONTAL) font_selector_box.Add(self.font_chooser, 4, wx.EXPAND | wx.TOP | wx.BOTTOM | wx.RIGHT, 10) - self.filter_label = wx.StaticText(self, wx.ID_ANY, _("Filter")) - font_selector_box.Add(self.filter_label, 0, wx.LEFT | wx.ALIGN_CENTRE_VERTICAL, 0) - font_selector_box.Add(self.font_filter, 1, wx.LEFT | wx.ALIGN_CENTRE_VERTICAL, 5) font_selector_sizer.Add(font_selector_box, 0, wx.EXPAND | wx.LEFT | wx.TOP | wx.RIGHT, 10) font_selector_sizer.Add(self.font_description, 1, wx.EXPAND | wx.ALL, 10) outer_sizer.Add(font_selector_sizer, 0, wx.EXPAND | wx.LEFT | wx.TOP | wx.RIGHT, 10) + # filter fon list + filter_sizer = wx.StaticBoxSizer(self.filter_box, wx.HORIZONTAL) + filter_size_label = wx.StaticText(self, wx.ID_ANY, _("Size")) + filter_sizer.Add(filter_size_label, 0, wx.LEFT | wx.TOP | wx.BOTTOM, 10) + filter_sizer.AddSpacer(5) + filter_sizer.Add(self.font_size_filter, 1, wx.RIGHT | wx.TOP | wx.BOTTOM, 10) + filter_sizer.AddSpacer(5) + filter_sizer.Add(self.font_glyph_filter, 1, wx.RIGHT | wx.TOP | wx.BOTTOM, 10) + filter_sizer.Add(self.font_category_filter, 1, wx.RIGHT | wx.TOP | wx.BOTTOM, 10) + outer_sizer.Add(filter_sizer, 0, wx.EXPAND | wx.LEFT | wx.TOP | wx.RIGHT, 10) + # options left_option_sizer = wx.BoxSizer(wx.VERTICAL) left_option_sizer.Add(self.back_and_forth_checkbox, 1, wx.EXPAND | wx.LEFT | wx.TOP | wx.RIGHT, 5) trim_option_sizer = wx.BoxSizer(wx.HORIZONTAL) - trim_option_sizer.Add(wx.StaticText(self, wx.ID_ANY, _("Add trims")), 0, wx.LEFT | wx.ALIGN_CENTRE_VERTICAL, 5) + trim_option_sizer.Add(wx.StaticText(self, wx.ID_ANY, _("Add trims")), 0, wx.LEFT | wx.ALIGN_TOP, 5) trim_option_sizer.Add(self.trim_option_choice, 1, wx.EXPAND | wx.LEFT | wx.TOP | wx.RIGHT | wx.BOTTOM, 5) trim_option_sizer.Add(self.use_trim_symbols, 1, wx.EXPAND | wx.LEFT | wx.TOP | wx.RIGHT | wx.BOTTOM, 5) left_option_sizer.Add(trim_option_sizer, 0, wx.ALIGN_LEFT, 5) diff --git a/lib/extensions/lettering_generate_json.py b/lib/extensions/lettering_generate_json.py index 85c918b2..a884ccac 100644 --- a/lib/extensions/lettering_generate_json.py +++ b/lib/extensions/lettering_generate_json.py @@ -10,7 +10,8 @@ import sys from inkex import Boolean from ..i18n import _ -from ..lettering.kerning import FontKerning +from ..lettering.categories import FONT_CATEGORIES +from ..lettering.font_info import FontFileInfo from .base import InkstitchExtension @@ -20,6 +21,11 @@ class LetteringGenerateJson(InkstitchExtension): ''' def __init__(self, *args, **kwargs): InkstitchExtension.__init__(self, *args, **kwargs) + self.arg_parser.add_argument("--options") + self.arg_parser.add_argument("--general") + self.arg_parser.add_argument("--settings") + self.arg_parser.add_argument("--kerning") + self.arg_parser.add_argument("-n", "--font-name", type=str, default="Font", dest="font_name") self.arg_parser.add_argument("-d", "--font-description", type=str, default="Description", dest="font_description") self.arg_parser.add_argument("-s", "--auto-satin", type=Boolean, default="true", dest="auto_satin") @@ -35,6 +41,9 @@ class LetteringGenerateJson(InkstitchExtension): self.arg_parser.add_argument("-w", "--word-spacing", type=int, default=26, dest="word_spacing") self.arg_parser.add_argument("-p", "--font-file", type=str, default="", dest="path") + for category in FONT_CATEGORIES: + self.arg_parser.add_argument(f"--{category.id}", type=Boolean, default="false", dest=category.id) + def effect(self): # file paths path = self.options.path @@ -43,20 +52,20 @@ class LetteringGenerateJson(InkstitchExtension): return output_path = os.path.join(os.path.dirname(path), 'font.json') - # kerning - kerning = FontKerning(path) + # font info (kerning, glyphs) + font_info = FontFileInfo(path) - horiz_adv_x = kerning.horiz_adv_x() - hkern = kerning.hkern() + horiz_adv_x = font_info.horiz_adv_x() + hkern = font_info.hkern() custom_leading = self.options.use_custom_leading custom_spacing = self.options.use_custom_spacing - word_spacing = kerning.word_spacing() + word_spacing = font_info.word_spacing() # use user input in case that the default word spacing is not defined # in the svg file or the user forces custom values if custom_spacing or not word_spacing: word_spacing = self.options.word_spacing - letter_spacing = kerning.letter_spacing() - units_per_em = kerning.units_per_em() or self.options.leading + letter_spacing = font_info.letter_spacing() + units_per_em = font_info.units_per_em() or self.options.leading # use units_per_em for leading (line height) if defined in the font file, # unless the user wishes to overwrite the value if units_per_em and not custom_leading: @@ -64,9 +73,17 @@ class LetteringGenerateJson(InkstitchExtension): else: leading = self.options.leading + glyphs = font_info.glyph_list() + + keywords = [] + for category in FONT_CATEGORIES: + if getattr(self.options, category.id): + keywords.append(category.id) + # collect data data = {'name': self.options.font_name, 'description': self.options.font_description, + 'keywords': keywords, 'leading': leading, 'auto_satin': self.options.auto_satin, 'reversible': self.options.reversible, @@ -79,7 +96,8 @@ class LetteringGenerateJson(InkstitchExtension): 'horiz_adv_x_space': word_spacing, 'units_per_em': units_per_em, 'horiz_adv_x': horiz_adv_x, - 'kerning_pairs': hkern + 'kerning_pairs': hkern, + 'glyphs': glyphs } # write data to font.json into the same directory as the font file diff --git a/lib/extensions/lettering_update_json_glyphlist.py b/lib/extensions/lettering_update_json_glyphlist.py new file mode 100644 index 00000000..4dea6d51 --- /dev/null +++ b/lib/extensions/lettering_update_json_glyphlist.py @@ -0,0 +1,41 @@ +# Authors: see git history +# +# Copyright (c) 2010 Authors +# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. + +import json +import os +import sys + +from ..i18n import _ +from ..lettering.font_info import FontFileInfo +from .base import InkstitchExtension + + +class LetteringUpdateJsonGlyphlist(InkstitchExtension): + ''' + This extension helps font creators to generate the json file for the lettering tool + ''' + def __init__(self, *args, **kwargs): + InkstitchExtension.__init__(self, *args, **kwargs) + self.arg_parser.add_argument("-f", "--font-file", type=str, default="", dest="font_file") + self.arg_parser.add_argument("-j", "--json-file", type=str, default="", dest="json_file") + + def effect(self): + # file paths + font_file = self.options.font_file + json_file = self.options.json_file + if not os.path.isfile(font_file) or not os.path.isfile(json_file): + print(_("Please verify file locations."), file=sys.stderr) + return + + glyphs = FontFileInfo(font_file).glyph_list() + + with open(json_file, 'r') as font_data: + data = json.load(font_data) + + data['glyphs'] = glyphs + + # write data to font.json into the same directory as the font file + with open(json_file, 'w', encoding="utf8") as font_data: + json.dump(data, font_data, indent=4, ensure_ascii=False) diff --git a/lib/inx/extensions.py b/lib/inx/extensions.py index 0ff3e889..30fca4da 100755 --- a/lib/inx/extensions.py +++ b/lib/inx/extensions.py @@ -8,6 +8,7 @@ import pyembroidery from ..commands import (COMMANDS, GLOBAL_COMMANDS, LAYER_COMMANDS, OBJECT_COMMANDS) from ..extensions import Input, Output, extensions +from ..lettering.categories import FONT_CATEGORIES from ..threads import ThreadCatalog from .outputs import pyembroidery_output_formats from .utils import build_environment, write_inx_file @@ -51,6 +52,7 @@ def generate_extension_inx_files(): write_inx_file(name, template.render(formats=pyembroidery_output_formats(), debug_formats=pyembroidery_debug_formats(), threadcatalog=threadcatalog(), + font_categories=FONT_CATEGORIES, layer_commands=layer_commands(), object_commands=object_commands(), global_commands=global_commands())) diff --git a/lib/lettering/categories.py b/lib/lettering/categories.py new file mode 100644 index 00000000..40b41529 --- /dev/null +++ b/lib/lettering/categories.py @@ -0,0 +1,30 @@ +# Authors: see git history +# +# Copyright (c) 2023 Authors +# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. + +from ..i18n import _ + + +class FontCategory: + def __init__(self, cat_id=None, name=None): + self.id: str = cat_id + self.name: str = name + + def __repr__(self): + return "FontCategory(%s, %s)" % (self.id, self.name) + + +FONT_CATEGORIES = [ + FontCategory('applique', _("Applique")), + FontCategory('crossstitch', _("Crossstitch")), + FontCategory('display', _('Display')), + FontCategory('handwriting', _("Handwriting")), + FontCategory('italic', _("Italic")), + FontCategory('monogram', _("Monogram")), + FontCategory('multicolor', _('Multicolor')), + FontCategory('running_stitch', _('Running Stitch')), + FontCategory('sans_serif', _("Sans Serif")), + FontCategory('serif', _("Serif")), + FontCategory('tiny', _("Tiny")) +] diff --git a/lib/lettering/font.py b/lib/lettering/font.py index 77f17e7f..fb17f760 100644 --- a/lib/lettering/font.py +++ b/lib/lettering/font.py @@ -111,6 +111,7 @@ class Font(object): name = localized_font_metadata('name', '') description = localized_font_metadata('description', '') + keywords = font_metadata('keywords', '') letter_case = font_metadata('letter_case', '') default_glyph = font_metadata('default_glyph', "�") leading = font_metadata('leading', 100) @@ -119,6 +120,7 @@ class Font(object): min_scale = font_metadata('min_scale', 1.0) max_scale = font_metadata('max_scale', 1.0) size = font_metadata('size', 0) + available_glyphs = font_metadata('glyphs', []) # use values from SVG Font, example: # <font horiz-adv-x="45" ... <glyph .... horiz-adv-x="49" glyph-name="A" /> ... <hkern ... k="3"g1="A" g2="B" /> .... /> diff --git a/lib/lettering/kerning.py b/lib/lettering/font_info.py index 5596ce8a..398786a4 100644 --- a/lib/lettering/kerning.py +++ b/lib/lettering/font_info.py @@ -5,9 +5,10 @@ from inkex import NSS from lxml import etree +from ..svg.tags import INKSCAPE_LABEL -class FontKerning(object): +class FontFileInfo(object): """ This class reads kerning information from an SVG file """ @@ -123,3 +124,14 @@ class FontKerning(object): xpath = "string(.//svg:missing-glyph/@*[name()='horiz-adv-x'])" return float(self.svg.xpath(xpath, namespaces=NSS)) """ + + def glyph_list(self): + """ + Returns a list of available glyphs in the font file + """ + glyphs = [] + glyph_layers = self.svg.xpath(".//svg:g[starts-with(@inkscape:label, 'GlyphLayer-')]", namespaces=NSS) + for layer in glyph_layers: + glyph_name = layer.attrib[INKSCAPE_LABEL].replace("GlyphLayer-", "", 1) + glyphs.append(glyph_name) + return glyphs diff --git a/lib/stitches/auto_fill.py b/lib/stitches/auto_fill.py index 03930ddd..50eea18e 100644 --- a/lib/stitches/auto_fill.py +++ b/lib/stitches/auto_fill.py @@ -17,9 +17,11 @@ from shapely.strtree import STRtree from ..debug import debug from ..stitch_plan import Stitch from ..svg import PIXELS_PER_MM +from ..utils.clamp_path import clamp_path_to_polygon from ..utils.geometry import Point as InkstitchPoint, line_string_to_point_list, ensure_multi_line_string from .fill import intersect_region_with_grating, stitch_row from .running_stitch import running_stitch +from ..utils.smoothing import smooth_path from ..utils.threading import check_stop_flag @@ -77,9 +79,9 @@ def auto_fill(shape, travel_graph = build_travel_graph(fill_stitch_graph, shape, angle, underpath) path = find_stitch_path(fill_stitch_graph, travel_graph, starting_point, ending_point) - result = path_to_stitches(path, travel_graph, fill_stitch_graph, angle, row_spacing, + result = path_to_stitches(shape, path, travel_graph, fill_stitch_graph, angle, row_spacing, max_stitch_length, running_stitch_length, running_stitch_tolerance, - staggers, skip_last) + staggers, skip_last, underpath) return result @@ -350,9 +352,9 @@ def get_segments(graph): def process_travel_edges(graph, fill_stitch_graph, shape, travel_edges): """Weight the interior edges and pre-calculate intersection with fill stitch rows.""" - # Set the weight equal to 5x the edge length, to encourage travel() + # Set the weight equal to 3x the edge length, to encourage travel() # to avoid them. - weight_edges_by_length(graph, 5) + weight_edges_by_length(graph, 3) segments = get_segments(fill_stitch_graph) @@ -618,20 +620,26 @@ def collapse_sequential_outline_edges(path, graph): if not start_of_run: start_of_run = edge[0] - if start_of_run: + if start_of_run and start_of_run != edge[1]: # if we were still in a run, close it off new_path.append(PathEdge((start_of_run, edge[1]), "collapsed")) return new_path -def travel(travel_graph, edge, running_stitch_length, running_stitch_tolerance, skip_last): +def travel(shape, travel_graph, edge, running_stitch_length, running_stitch_tolerance, skip_last, underpath): """Create stitches to get from one point on an outline of the shape to another.""" start, end = edge path = networkx.shortest_path(travel_graph, start, end, weight='weight') - path = [Stitch(*p) for p in path] - stitches = running_stitch(path, running_stitch_length, running_stitch_tolerance) + if underpath and path != (start, end): + path = smooth_path(path, 2) + else: + path = [InkstitchPoint.from_tuple(point) for point in path] + path = clamp_path_to_polygon(path, shape) + + points = running_stitch(path, running_stitch_length, running_stitch_tolerance) + stitches = [Stitch(point) for point in points] for stitch in stitches: stitch.add_tag('auto_fill_travel') @@ -653,8 +661,8 @@ def travel(travel_graph, edge, running_stitch_length, running_stitch_tolerance, @debug.time -def path_to_stitches(path, travel_graph, fill_stitch_graph, angle, row_spacing, max_stitch_length, running_stitch_length, running_stitch_tolerance, - staggers, skip_last): +def path_to_stitches(shape, path, travel_graph, fill_stitch_graph, angle, row_spacing, max_stitch_length, running_stitch_length, + running_stitch_tolerance, staggers, skip_last, underpath): path = collapse_sequential_outline_edges(path, fill_stitch_graph) stitches = [] @@ -668,7 +676,7 @@ def path_to_stitches(path, travel_graph, fill_stitch_graph, angle, row_spacing, stitch_row(stitches, edge[0], edge[1], angle, row_spacing, max_stitch_length, staggers, skip_last) travel_graph.remove_edges_from(fill_stitch_graph[edge[0]][edge[1]]['segment'].get('underpath_edges', [])) else: - stitches.extend(travel(travel_graph, edge, running_stitch_length, running_stitch_tolerance, skip_last)) + stitches.extend(travel(shape, travel_graph, edge, running_stitch_length, running_stitch_tolerance, skip_last, underpath)) check_stop_flag() diff --git a/lib/stitches/circular_fill.py b/lib/stitches/circular_fill.py index e63f5607..351f4cf4 100644 --- a/lib/stitches/circular_fill.py +++ b/lib/stitches/circular_fill.py @@ -78,7 +78,7 @@ def circular_fill(shape, travel_graph = build_travel_graph(fill_stitch_graph, shape, angle, underpath) path = find_stitch_path(fill_stitch_graph, travel_graph, starting_point, ending_point) - result = path_to_stitches(path, travel_graph, fill_stitch_graph, running_stitch_length, running_stitch_tolerance, skip_last) + result = path_to_stitches(shape, path, travel_graph, fill_stitch_graph, running_stitch_length, running_stitch_tolerance, skip_last, underpath) result = _apply_bean_stitch_and_repeats(result, repeats, bean_stitch_repeats) return result @@ -117,7 +117,7 @@ def _get_start_end_sequence(outline, start, end): return substring(outline, start_dist, end_dist) -def path_to_stitches(path, travel_graph, fill_stitch_graph, running_stitch_length, running_stitch_tolerance, skip_last): +def path_to_stitches(shape, path, travel_graph, fill_stitch_graph, running_stitch_length, running_stitch_tolerance, skip_last, underpath): path = collapse_sequential_outline_edges(path, fill_stitch_graph) stitches = [] @@ -144,6 +144,6 @@ def path_to_stitches(path, travel_graph, fill_stitch_graph, running_stitch_lengt travel_graph.remove_edges_from(fill_stitch_graph[edge[0]][edge[1]]['segment'].get('underpath_edges', [])) else: - stitches.extend(travel(travel_graph, edge, running_stitch_length, running_stitch_tolerance, skip_last)) + stitches.extend(travel(shape, travel_graph, edge, running_stitch_length, running_stitch_tolerance, skip_last, underpath)) return stitches diff --git a/lib/stitches/contour_fill.py b/lib/stitches/contour_fill.py index 2ea61afb..993fa104 100644 --- a/lib/stitches/contour_fill.py +++ b/lib/stitches/contour_fill.py @@ -569,6 +569,7 @@ def _make_fermat_spiral(rings, stitch_length, starting_point): def _make_spiral(rings, stitch_length, starting_point): path = [] + spiral_part = None for ring1, ring2 in zip(rings[:-1], rings[1:]): check_stop_flag() @@ -577,7 +578,8 @@ def _make_spiral(rings, stitch_length, starting_point): # skip last to avoid duplicated points path.extend(spiral_part.coords[:-1]) - # at the end add last point - path.append(spiral_part.coords[-1]) + if spiral_part: + # at the end add last point + path.append(spiral_part.coords[-1]) return path diff --git a/lib/stitches/guided_fill.py b/lib/stitches/guided_fill.py index 7b80c021..e4793838 100644 --- a/lib/stitches/guided_fill.py +++ b/lib/stitches/guided_fill.py @@ -45,7 +45,9 @@ def guided_fill(shape, travel_graph = build_travel_graph(fill_stitch_graph, shape, angle, underpath) path = find_stitch_path(fill_stitch_graph, travel_graph, starting_point, ending_point) - result = path_to_stitches(path, travel_graph, fill_stitch_graph, max_stitch_length, running_stitch_length, running_stitch_tolerance, skip_last) + result = path_to_stitches(shape, path, travel_graph, fill_stitch_graph, + max_stitch_length, running_stitch_length, running_stitch_tolerance, skip_last, + underpath) return result @@ -59,7 +61,9 @@ def fallback(shape, guideline, row_spacing, max_stitch_length, running_stitch_le num_staggers, skip_last, starting_point, ending_point, underpath) -def path_to_stitches(path, travel_graph, fill_stitch_graph, stitch_length, running_stitch_length, running_stitch_tolerance, skip_last): +def path_to_stitches(shape, path, travel_graph, fill_stitch_graph, + stitch_length, running_stitch_length, running_stitch_tolerance, skip_last, + underpath): path = collapse_sequential_outline_edges(path, fill_stitch_graph) stitches = [] @@ -89,7 +93,7 @@ def path_to_stitches(path, travel_graph, fill_stitch_graph, stitch_length, runni travel_graph.remove_edges_from(fill_stitch_graph[edge[0]][edge[1]]['segment'].get('underpath_edges', [])) else: - stitches.extend(travel(travel_graph, edge, running_stitch_length, running_stitch_tolerance, skip_last)) + stitches.extend(travel(shape, travel_graph, edge, running_stitch_length, running_stitch_tolerance, skip_last, underpath)) return stitches diff --git a/lib/stitches/running_stitch.py b/lib/stitches/running_stitch.py index 50e3be5f..b6262a45 100644 --- a/lib/stitches/running_stitch.py +++ b/lib/stitches/running_stitch.py @@ -11,7 +11,6 @@ from copy import copy import numpy as np from shapely import geometry as shgeo -from ..debug import debug from ..utils import prng from ..utils.geometry import Point from ..utils.threading import check_stop_flag @@ -248,9 +247,10 @@ def path_to_curves(points: typing.List[Point], min_len: float): return curves -@debug.time def running_stitch(points, stitch_length, tolerance): # Turn a continuous path into a running stitch. + if not points: + return stitches = [points[0]] for curve in path_to_curves(points, 2 * tolerance): # segments longer than twice the tollerance will usually be forced by it, so set that as the minimum for corner detection diff --git a/lib/threads/color.py b/lib/threads/color.py index 8dc1ea01..c75778d2 100644 --- a/lib/threads/color.py +++ b/lib/threads/color.py @@ -6,19 +6,24 @@ import colorsys import re -import tinycss2.color3 -from pyembroidery.EmbThread import EmbThread - from inkex import Color +from pyembroidery.EmbThread import EmbThread class ThreadColor(object): hex_str_re = re.compile('#([0-9a-z]{3}|[0-9a-z]{6})', re.I) def __init__(self, color, name=None, number=None, manufacturer=None, description=None, chart=None): - # set colors with a gradient to black (avoiding an error message) - if type(color) == str and color.startswith('url'): + ''' + avoid error messages: + * set colors with a gradient to black + * currentColor should not just be black, but we want to avoid error messages + until inkex will be able to handle this css property + ''' + if type(color) == str and color.startswith(('url', 'currentColor')): color = None + elif type(color) == str and color.startswith('rgb'): + color = tuple(int(value) for value in color[4:-1].split(',')) if color is None: self.rgb = (0, 0, 0) @@ -31,9 +36,7 @@ class ThreadColor(object): self.rgb = (color.get_red(), color.get_green(), color.get_blue()) return elif isinstance(color, str): - self.rgb = tinycss2.color3.parse_color(color) - # remove alpha channel and multiply with 255 - self.rgb = tuple(channel * 255.0 for channel in list(self.rgb)[:-1]) + self.rgb = Color.parse_str(color)[1] elif isinstance(color, (list, tuple)): self.rgb = tuple(color) elif self.hex_str_re.match(color): diff --git a/lib/utils/clamp_path.py b/lib/utils/clamp_path.py index e5ef78d8..f9b8991a 100644 --- a/lib/utils/clamp_path.py +++ b/lib/utils/clamp_path.py @@ -1,6 +1,6 @@ from shapely.geometry import LineString, Point as ShapelyPoint, MultiPolygon from shapely.prepared import prep -from .geometry import Point, ensure_multi_line_string +from .geometry import Point, ensure_geometry_collection def path_to_segments(path): @@ -66,23 +66,34 @@ def find_border(polygon, point): def clamp_path_to_polygon(path, polygon): """Constrain a path to a Polygon. + The path is expected to have at least some part inside the Polygon. + Description: https://gis.stackexchange.com/questions/428848/clamp-linestring-to-polygon """ - path = LineString(path) + start = path[0] + end = path[-1] # This splits the path at the points where it intersects with the polygon # border and returns the pieces in the same order as the original path. - split_path = ensure_multi_line_string(path.difference(polygon.boundary)) + split_path = ensure_geometry_collection(LineString(path).difference(polygon.boundary)) + + if len(split_path.geoms) == 1: + # The path never intersects with the polygon, so it's entirely inside. + return path + + # Add the start and end points to avoid losing part of the path if the + # start or end coincides with the polygon boundary + split_path = [ShapelyPoint(start), *split_path.geoms, ShapelyPoint(end)] - # contains() checks can fail without this. + # contains() checks can fail without the buffer. buffered_polygon = prep(polygon.buffer(1e-9)) last_segment_inside = None was_inside = False result = [] - for segment in split_path.geoms: + for segment in split_path: if buffered_polygon.contains(segment): if not was_inside: if last_segment_inside is not None: diff --git a/lib/utils/smoothing.py b/lib/utils/smoothing.py index 1bb250c5..2c210e37 100644 --- a/lib/utils/smoothing.py +++ b/lib/utils/smoothing.py @@ -3,7 +3,6 @@ from scipy.interpolate import splprep, splev from .geometry import Point, coordinate_list_to_point_list from ..stitches.running_stitch import running_stitch -from ..debug import debug def _remove_duplicate_coordinates(coords_array): @@ -23,7 +22,6 @@ def _remove_duplicate_coordinates(coords_array): return coords_array[keepers] -@debug.time def smooth_path(path, smoothness=1.0): """Smooth a path of coordinates. @@ -70,8 +68,7 @@ def smooth_path(path, smoothness=1.0): # .T transposes the array (for some reason splprep expects # [[x1, x2, ...], [y1, y2, ...]] - with debug.time_this("splprep"): - tck, fp, ier, msg = splprep(coords.T, s=s, k=3, nest=-1, full_output=1) + tck, fp, ier, msg = splprep(coords.T, s=s, k=3, nest=-1, full_output=1) if ier > 0: debug.log(f"error {ier} smoothing path: {msg}") return path @@ -79,8 +76,7 @@ def smooth_path(path, smoothness=1.0): # Evaluate the spline curve at many points along its length to produce the # smoothed point list. 2 * num_points seems to be a good number, but it # does produce a lot of points. - with debug.time_this("splev"): - smoothed_x_values, smoothed_y_values = splev(np.linspace(0, 1, int(num_points * 2)), tck[0]) - coords = np.array([smoothed_x_values, smoothed_y_values]).T + smoothed_x_values, smoothed_y_values = splev(np.linspace(0, 1, int(num_points * 2)), tck[0]) + coords = np.array([smoothed_x_values, smoothed_y_values]).T return [Point(x, y) for x, y in coords] |
