diff options
Diffstat (limited to 'lib/extensions')
| -rw-r--r-- | lib/extensions/base.py | 5 | ||||
| -rw-r--r-- | lib/extensions/convert_to_satin.py | 71 | ||||
| -rw-r--r-- | lib/extensions/gradient_blocks.py | 7 | ||||
| -rw-r--r-- | lib/extensions/letters_to_font.py | 18 | ||||
| -rw-r--r-- | lib/extensions/object_commands_toggle_visibility.py | 2 | ||||
| -rw-r--r-- | lib/extensions/params.py | 20 | ||||
| -rw-r--r-- | lib/extensions/preferences.py | 26 | ||||
| -rw-r--r-- | lib/extensions/remove_embroidery_settings.py | 67 | ||||
| -rw-r--r-- | lib/extensions/stroke_to_lpe_satin.py | 2 | ||||
| -rw-r--r-- | lib/extensions/zigzag_line_to_satin.py | 16 | ||||
| -rw-r--r-- | lib/extensions/zip.py | 39 |
11 files changed, 171 insertions, 102 deletions
diff --git a/lib/extensions/base.py b/lib/extensions/base.py index 3c16a11c..e0bf4131 100644 --- a/lib/extensions/base.py +++ b/lib/extensions/base.py @@ -7,7 +7,6 @@ import os import inkex from lxml.etree import Comment -from stringcase import snakecase from ..commands import is_command, layer_commands from ..elements import EmbroideryElement, nodes_to_elements @@ -32,7 +31,9 @@ class InkstitchExtension(inkex.EffectExtension): @classmethod def name(cls): - return snakecase(cls.__name__) + # Convert CamelCase to snake_case + return cls.__name__[0].lower() + ''.join([x if x.islower() else f'_{x.lower()}' + for x in cls.__name__[1:]]) def hide_all_layers(self): for g in self.document.getroot().findall(SVG_GROUP_TAG): diff --git a/lib/extensions/convert_to_satin.py b/lib/extensions/convert_to_satin.py index 7a36ce21..4bb3588e 100644 --- a/lib/extensions/convert_to_satin.py +++ b/lib/extensions/convert_to_satin.py @@ -13,7 +13,7 @@ from numpy import diff, setdiff1d, sign from shapely import geometry as shgeo from .base import InkstitchExtension -from ..elements import Stroke +from ..elements import SatinColumn, Stroke from ..i18n import _ from ..svg import PIXELS_PER_MM, get_correction_transform from ..svg.tags import INKSTITCH_ATTRIBS @@ -51,22 +51,28 @@ class ConvertToSatin(InkstitchExtension): path_style = self.path_style(element) for path in element.paths: - path = self.remove_duplicate_points(path) + path = self.remove_duplicate_points(self.fix_loop(path)) if len(path) < 2: # ignore paths with just one point -- they're not visible to the user anyway continue - for satin in self.convert_path_to_satins(path, element.stroke_width, style_args, correction_transform, path_style): - parent.insert(index, satin) - index += 1 + satins = list(self.convert_path_to_satins(path, element.stroke_width, style_args, path_style)) + + if satins: + joined_satin = satins[0] + for satin in satins[1:]: + joined_satin = joined_satin.merge(satin) + + joined_satin.node.set('transform', correction_transform) + parent.insert(index, joined_satin.node) parent.remove(element.node) - def convert_path_to_satins(self, path, stroke_width, style_args, correction_transform, path_style, depth=0): + def convert_path_to_satins(self, path, stroke_width, style_args, path_style, depth=0): try: rails, rungs = self.path_to_satin(path, stroke_width, style_args) - yield self.satin_to_svg_node(rails, rungs, correction_transform, path_style) + yield SatinColumn(self.satin_to_svg_node(rails, rungs, path_style)) except SelfIntersectionError: # The path intersects itself. Split it in two and try doing the halves # individually. @@ -76,27 +82,37 @@ class ConvertToSatin(InkstitchExtension): # getting nowhere. Just give up on this section of the path. return - half = int(len(path) / 2.0) - halves = [path[:half + 1], path[half:]] + halves = self.split_path(path) for path in halves: - for satin in self.convert_path_to_satins(path, stroke_width, style_args, correction_transform, path_style, depth=depth + 1): + for satin in self.convert_path_to_satins(path, stroke_width, style_args, path_style, depth=depth + 1): yield satin + def split_path(self, path): + half = len(path) // 2 + halves = [path[:half], path[half:]] + + start = Point.from_tuple(halves[0][-1]) + end = Point.from_tuple(halves[1][0]) + + midpoint = (start + end) / 2 + midpoint = midpoint.as_tuple() + + halves[0].append(midpoint) + halves[1] = [midpoint] + halves[1] + + return halves + def fix_loop(self, path): - if path[0] == path[-1]: - # Looping paths seem to confuse shapely's parallel_offset(). It loses track - # of where the start and endpoint is, even if the user explicitly breaks the - # path. I suspect this is because parallel_offset() uses buffer() under the - # hood. - # - # To work around this we'll introduce a tiny gap by nudging the starting point - # toward the next point slightly. - start = Point(*path[0]) - next = Point(*path[1]) - direction = (next - start).unit() - start += 0.01 * direction - path[0] = start.as_tuple() + if path[0] == path[-1] and len(path) > 1: + first = Point.from_tuple(path[0]) + second = Point.from_tuple(path[1]) + midpoint = (first + second) / 2 + midpoint = midpoint.as_tuple() + + return [midpoint] + path[1:] + [path[0], midpoint] + else: + return path def remove_duplicate_points(self, path): path = [[round(coord, 4) for coord in point] for point in path] @@ -304,10 +320,8 @@ class ConvertToSatin(InkstitchExtension): # Rotate 90 degrees left to make a normal vector. normal = tangent.rotate_left() - # Travel 75% of the stroke width left and right to make the rung's - # endpoints. This means the rung's length is 150% of the stroke - # width. - offset = normal * stroke_width * 0.75 + # Extend the rungs by an offset value to make sure they will cross the rails + offset = normal * (stroke_width / 2) * 1.2 rung_start = rung_center + offset rung_end = rung_center - offset @@ -319,7 +333,7 @@ class ConvertToSatin(InkstitchExtension): color = element.get_style('stroke', '#000000') return "stroke:%s;stroke-width:1px;fill:none" % (color) - def satin_to_svg_node(self, rails, rungs, correction_transform, path_style): + def satin_to_svg_node(self, rails, rungs, path_style): d = "" for path in chain(rails, rungs): d += "M" @@ -330,7 +344,6 @@ class ConvertToSatin(InkstitchExtension): return inkex.PathElement(attrib={ "id": self.uniqueId("path"), "style": path_style, - "transform": correction_transform, "d": d, INKSTITCH_ATTRIBS['satin_column']: "true", }) diff --git a/lib/extensions/gradient_blocks.py b/lib/extensions/gradient_blocks.py index b8142d7f..f94582f0 100644 --- a/lib/extensions/gradient_blocks.py +++ b/lib/extensions/gradient_blocks.py @@ -52,7 +52,7 @@ class GradientBlocks(CommandsExtension): correction_transform = get_correction_transform(element.node) style = element.node.style index = parent.index(element.node) - fill_shapes, attributes = gradient_shapes_and_attributes(element, element.shape) + fill_shapes, attributes = gradient_shapes_and_attributes(element, element.shape, self.svg.viewport_to_unit(1)) # reverse order so we can always insert with the same index number fill_shapes.reverse() attributes.reverse() @@ -127,7 +127,7 @@ class GradientBlocks(CommandsExtension): return path -def gradient_shapes_and_attributes(element, shape): +def gradient_shapes_and_attributes(element, shape, unit_multiplier): # e.g. url(#linearGradient872) -> linearGradient872 color = element.color[5:-1] xpath = f'.//svg:defs/svg:linearGradient[@id="{color}"]' @@ -144,6 +144,7 @@ def gradient_shapes_and_attributes(element, shape): # create bbox polygon to calculate the length necessary to make sure that # the gradient splitter lines will cut the entire design + # bounding_box returns the value in viewport units, we need to convert the length later to px bbox = element.node.bounding_box() bbox_polygon = shgeo.Polygon([(bbox.left, bbox.top), (bbox.right, bbox.top), (bbox.right, bbox.bottom), (bbox.left, bbox.bottom)]) @@ -159,7 +160,7 @@ def gradient_shapes_and_attributes(element, shape): for i, offset in enumerate(offsets): shape_rest = [] split_point = shgeo.Point(line.point_at_ratio(float(offset))) - length = split_point.hausdorff_distance(bbox_polygon) + length = split_point.hausdorff_distance(bbox_polygon) / unit_multiplier split_line = shgeo.LineString([(split_point.x - length - 2, split_point.y), (split_point.x + length + 2, split_point.y)]) split_line = rotate(split_line, angle, origin=split_point, use_radians=True) diff --git a/lib/extensions/letters_to_font.py b/lib/extensions/letters_to_font.py index 56a33ad8..d4d9e60a 100644 --- a/lib/extensions/letters_to_font.py +++ b/lib/extensions/letters_to_font.py @@ -39,6 +39,7 @@ class LettersToFont(InkstitchExtension): glyphs = list(Path(font_dir).rglob(file_format.lower())) document = self.document.getroot() + group = None for glyph in glyphs: letter = self.get_glyph_element(glyph) label = "GlyphLayer-%s" % letter.get(INKSCAPE_LABEL, ' ').split('.')[0][-1] @@ -59,15 +60,20 @@ class LettersToFont(InkstitchExtension): document.insert(0, group) group.set('style', 'display:none') + # We found no glyphs, no need to proceed + if group is None: + return + # users may be confused if they get an empty document # make last letter visible again group.set('style', None) - # In most cases trims are inserted with the imported letters. - # Let's make sure the trim symbol exists in the defs section - ensure_symbol(document, 'trim') + if self.options.import_commands == "symbols": + # In most cases trims are inserted with the imported letters. + # Let's make sure the trim symbol exists in the defs section + ensure_symbol(document, 'trim') - self.insert_baseline(document) + self.insert_baseline() def get_glyph_element(self, glyph): stitch_plan = generate_stitch_plan(str(glyph), self.options.import_commands) @@ -77,5 +83,5 @@ class LettersToFont(InkstitchExtension): stitch_plan.attrib.pop(INKSCAPE_GROUPMODE) return stitch_plan - def insert_baseline(self, document): - document.namedview.add_guide(position=0.0, name="baseline") + def insert_baseline(self): + self.svg.namedview.add_guide(position=0.0, name="baseline") diff --git a/lib/extensions/object_commands_toggle_visibility.py b/lib/extensions/object_commands_toggle_visibility.py index 569f4305..e5d247e6 100644 --- a/lib/extensions/object_commands_toggle_visibility.py +++ b/lib/extensions/object_commands_toggle_visibility.py @@ -19,6 +19,6 @@ class ObjectCommandsToggleVisibility(InkstitchExtension): for command_group in command_groups: if first_iteration: first_iteration = False - if not command_group.is_visible(): + if command_group.style('display', 'inline') == 'none': display = "inline" command_group.style['display'] = display diff --git a/lib/extensions/params.py b/lib/extensions/params.py index 540cc7bb..1ba144b2 100644 --- a/lib/extensions/params.py +++ b/lib/extensions/params.py @@ -7,7 +7,6 @@ import os import sys -import traceback from collections import defaultdict from copy import copy from itertools import groupby, zip_longest @@ -20,6 +19,7 @@ from ..commands import is_command, is_command_symbol from ..elements import (Clone, EmbroideryElement, FillStitch, Polyline, SatinColumn, Stroke) from ..elements.clone import is_clone +from ..exceptions import InkstitchException, format_uncaught_exception from ..gui import PresetsPanel, SimulatorPreview, WarningPanel from ..i18n import _ from ..svg.tags import SVG_POLYLINE_TAG @@ -544,24 +544,22 @@ class SettingsFrame(wx.Frame): patches.extend(copy(node).embroider(None)) check_stop_flag() - except SystemExit: - wx.CallAfter(self._show_warning) + except (SystemExit, ExitThread): raise - except ExitThread: - raise - except Exception as e: - # Ignore errors. This can be things like incorrect paths for - # satins or division by zero caused by incorrect param values. - traceback.print_exception(e, file=sys.stderr) - pass + except InkstitchException as exc: + wx.CallAfter(self._show_warning, str(exc)) + except Exception: + wx.CallAfter(self._show_warning, format_uncaught_exception()) return patches def _hide_warning(self): + self.warning_panel.clear() self.warning_panel.Hide() self.Layout() - def _show_warning(self): + def _show_warning(self, warning_text): + self.warning_panel.set_warning_text(warning_text) self.warning_panel.Show() self.Layout() diff --git a/lib/extensions/preferences.py b/lib/extensions/preferences.py index 44c1b5aa..b78537c8 100644 --- a/lib/extensions/preferences.py +++ b/lib/extensions/preferences.py @@ -4,8 +4,7 @@ # Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. from .base import InkstitchExtension -from ..api import APIServer -from ..gui import open_url +from ..gui.preferences import PreferencesApp class Preferences(InkstitchExtension): @@ -13,25 +12,6 @@ class Preferences(InkstitchExtension): This saves embroider settings into the metadata of the file ''' - def __init__(self, *args, **kwargs): - InkstitchExtension.__init__(self, *args, **kwargs) - self.arg_parser.add_argument("-c", "--collapse_len_mm", - action="store", type=float, - dest="collapse_length_mm", default=3.0, - help="max collapse length (mm)") - self.arg_parser.add_argument("-l", "--min_stitch_len_mm", - action="store", type=float, - dest="min_stitch_len_mm", default=0, - help="minimum stitch length (mm)") - def effect(self): - api_server = APIServer(self) - port = api_server.start_server() - electron = open_url("/preferences", port) - electron.wait() - api_server.stop() - api_server.join() - - # self.metadata = self.get_inkstitch_metadata() - # self.metadata['collapse_len_mm'] = self.options.collapse_length_mm - # self.metadata['min_stitch_len_mm'] = self.options.min_stitch_len_mm + app = PreferencesApp(self) + app.MainLoop() diff --git a/lib/extensions/remove_embroidery_settings.py b/lib/extensions/remove_embroidery_settings.py index d8e6cb0e..b90d590b 100644 --- a/lib/extensions/remove_embroidery_settings.py +++ b/lib/extensions/remove_embroidery_settings.py @@ -5,7 +5,7 @@ from inkex import NSS, Boolean, ShapeElement -from ..commands import find_commands +from ..commands import OBJECT_COMMANDS, find_commands from ..svg.svg import find_elements from .base import InkstitchExtension @@ -14,7 +14,7 @@ class RemoveEmbroiderySettings(InkstitchExtension): def __init__(self, *args, **kwargs): InkstitchExtension.__init__(self, *args, **kwargs) self.arg_parser.add_argument("-p", "--del_params", dest="del_params", type=Boolean, default=True) - self.arg_parser.add_argument("-c", "--del_commands", dest="del_commands", type=Boolean, default=False) + self.arg_parser.add_argument("-c", "--del_commands", dest="del_commands", type=str, default="none") self.arg_parser.add_argument("-d", "--del_print", dest="del_print", type=Boolean, default=False) def effect(self): @@ -22,7 +22,7 @@ class RemoveEmbroiderySettings(InkstitchExtension): if self.options.del_params: self.remove_params() - if self.options.del_commands: + if self.options.del_commands != 'none': self.remove_commands() if self.options.del_print: self.remove_print_settings() @@ -43,28 +43,53 @@ class RemoveEmbroiderySettings(InkstitchExtension): elements = self.get_selected_elements() self.remove_inkstitch_attributes(elements) - def remove_commands(self): - if not self.svg.selection: - # remove intact command groups - xpath = ".//svg:g[starts-with(@id,'command_group')]" - groups = find_elements(self.svg, xpath) - for group in groups: + def remove_all_commands(self): + xpath = ".//svg:g[starts-with(@id,'command_group')]" + groups = find_elements(self.svg, xpath) + for group in groups: + group.getparent().remove(group) + + # remove standalone commands and ungrouped object commands + standalone_commands = ".//svg:use[starts-with(@xlink:href, '#inkstitch_')]|.//svg:path[starts-with(@id, 'command_connector')]" + self.remove_elements(standalone_commands) + + # let's remove the symbols (defs), we won't need them in the document + symbols = ".//*[starts-with(@id, 'inkstitch_')]" + self.remove_elements(symbols) + + def remove_specific_commands(self, command): + # remove object commands + if command in OBJECT_COMMANDS: + xlink = f"#inkstitch_{command}" + xpath = f".//svg:use[starts-with(@xlink:href, '{xlink}')]" + connectors = find_elements(self.svg, xpath) + for connector in connectors: + group = connector.getparent() group.getparent().remove(group) - else: - elements = self.get_selected_elements() - for element in elements: - for command in find_commands(element): + + # remove standalone commands and ungrouped object commands + standalone_commands = ".//svg:use[starts-with(@xlink:href, '#inkstitch_{command}')]" + self.remove_elements(standalone_commands) + + # let's remove the symbols (defs), we won't need them in the document + symbols = f".//*[starts-with(@id, 'inkstitch_{command}')]" + self.remove_elements(symbols) + + def remove_selected_commands(self): + elements = self.get_selected_elements() + for element in elements: + for command in find_commands(element): + if self.options.del_commands in ('all', command.command): group = command.connector.getparent() group.getparent().remove(group) - if not self.svg.selection: - # remove standalone commands and ungrouped object commands - standalone_commands = ".//svg:use[starts-with(@xlink:href, '#inkstitch_')]|.//svg:path[starts-with(@id, 'command_connector')]" - self.remove_elements(standalone_commands) - - # let's remove the symbols (defs), we won't need them in the document - symbols = ".//*[starts-with(@id, 'inkstitch_')]" - self.remove_elements(symbols) + def remove_commands(self): + if self.svg.selection: + self.remove_selected_commands() + elif self.options.del_commands == "all": + self.remove_all_commands() + else: + self.remove_specific_commands(self.options.del_commands) def get_selected_elements(self): return self.svg.selection.get(ShapeElement) diff --git a/lib/extensions/stroke_to_lpe_satin.py b/lib/extensions/stroke_to_lpe_satin.py index b89e471c..96cab4e9 100644 --- a/lib/extensions/stroke_to_lpe_satin.py +++ b/lib/extensions/stroke_to_lpe_satin.py @@ -209,7 +209,7 @@ class SatinPattern: return str(path1) + str(path2) + rungs -satin_patterns = {'normal': SatinPattern('M 0,0.4 H 4 H 8', 'cc'), +satin_patterns = {'normal': SatinPattern('M 0,0 H 4 H 8', 'cc'), 'pearl': SatinPattern('M 0,0 C 0,0.22 0.18,0.4 0.4,0.4 0.62,0.4 0.8,0.22 0.8,0', 'csc'), 'diamond': SatinPattern('M 0,0 0.4,0.2 0.8,0', 'ccc'), 'triangle': SatinPattern('M 0,0 0.4,0.1 0.78,0.2 0.8,0', 'cccc'), diff --git a/lib/extensions/zigzag_line_to_satin.py b/lib/extensions/zigzag_line_to_satin.py index 167f4b91..b71bf6a0 100644 --- a/lib/extensions/zigzag_line_to_satin.py +++ b/lib/extensions/zigzag_line_to_satin.py @@ -23,11 +23,12 @@ class ZigzagLineToSatin(InkstitchExtension): self.arg_parser.add_argument("-l", "--reduce-rungs", type=inkex.Boolean, default=False, dest="reduce_rungs") def effect(self): - if not self.svg.selection or not self.get_elements(): + nodes = self.get_selection(self.svg.selection) + if not nodes: inkex.errormsg(_("Please select at least one stroke to convert to a satin column.")) return - for node in self.svg.selection: + for node in nodes: d = [] point_list = list(node.get_path().end_points) # find duplicated nodes (= do not smooth) @@ -49,6 +50,17 @@ class ZigzagLineToSatin(InkstitchExtension): node.set('d', " ".join(d)) node.set('inkstitch:satin_column', True) + def get_selection(self, nodes): + selection = [] + for node in nodes: + # we only apply to path elements, no use in converting ellipses or rectangles, etc. + if node.TAG == "path": + selection.append(node) + elif node.TAG == "g": + for element in node.descendants(): + selection.extend(self.get_selection(element)) + return selection + def _get_sharp_edge_nodes(self, point_list): points = [] sharp_edges = [] diff --git a/lib/extensions/zip.py b/lib/extensions/zip.py index e80bc34c..b3183a9a 100644 --- a/lib/extensions/zip.py +++ b/lib/extensions/zip.py @@ -9,15 +9,17 @@ import tempfile from copy import deepcopy from zipfile import ZipFile +from inkex import Boolean from lxml import etree import pyembroidery -from inkex import Boolean from ..i18n import _ from ..output import write_embroidery_file from ..stitch_plan import stitch_groups_to_stitch_plan +from ..svg import PIXELS_PER_MM from ..threads import ThreadCatalog +from ..utils.geometry import Point from .base import InkstitchExtension @@ -25,6 +27,11 @@ class Zip(InkstitchExtension): def __init__(self, *args, **kwargs): InkstitchExtension.__init__(self) + self.arg_parser.add_argument('--notebook', type=Boolean, default=True) + self.arg_parser.add_argument('--file-formats', type=Boolean, default=True) + self.arg_parser.add_argument('--panelization', type=Boolean, default=True) + self.arg_parser.add_argument('--output-options', type=Boolean, default=True) + # it's kind of obnoxious that I have to do this... self.formats = [] for format in pyembroidery.supported_formats(): @@ -33,10 +40,17 @@ class Zip(InkstitchExtension): self.arg_parser.add_argument('--format-%s' % extension, type=Boolean, dest=extension) self.formats.append(extension) self.arg_parser.add_argument('--format-svg', type=Boolean, dest='svg') - self.arg_parser.add_argument('--format-threadlist', type=Boolean, dest='threadlist') self.formats.append('svg') + self.arg_parser.add_argument('--format-threadlist', type=Boolean, dest='threadlist') self.formats.append('threadlist') + self.arg_parser.add_argument('--x-repeats', type=int, dest='x_repeats', default=1) + self.arg_parser.add_argument('--y-repeats', type=int, dest='y_repeats', default=1) + self.arg_parser.add_argument('--x-spacing', type=float, dest='x_spacing', default=100) + self.arg_parser.add_argument('--y-spacing', type=float, dest='y_spacing', default=100) + + self.arg_parser.add_argument('--custom-file-name', type=str, dest='custom_file_name', default='') + def effect(self): if not self.get_elements(): return @@ -47,7 +61,10 @@ class Zip(InkstitchExtension): patches = self.elements_to_stitch_groups(self.elements) stitch_plan = stitch_groups_to_stitch_plan(patches, collapse_len=collapse_len, min_stitch_len=min_stitch_len) - base_file_name = self.get_base_file_name() + if self.options.x_repeats != 1 or self.options.y_repeats != 1: + stitch_plan = self._make_offsets(stitch_plan) + + base_file_name = self._get_file_name() path = tempfile.mkdtemp() files = [] @@ -93,6 +110,22 @@ class Zip(InkstitchExtension): # don't let inkex output the SVG! sys.exit(0) + def _get_file_name(self): + if self.options.custom_file_name: + base_file_name = self.options.custom_file_name + else: + base_file_name = self.get_base_file_name() + return base_file_name + + def _make_offsets(self, stitch_plan): + dx = self.options.x_spacing * PIXELS_PER_MM + dy = self.options.y_spacing * PIXELS_PER_MM + offsets = [] + for x in range(self.options.x_repeats): + for y in range(self.options.y_repeats): + offsets.append(Point(x * dx, y * dy)) + return stitch_plan.make_offsets(offsets) + def get_threadlist(self, stitch_plan, design_name): ThreadCatalog().match_and_apply_palette(stitch_plan, self.get_inkstitch_metadata()['thread-palette']) thread_used = [] |
