diff options
Diffstat (limited to 'lib/extensions')
31 files changed, 992 insertions, 234 deletions
diff --git a/lib/extensions/__init__.py b/lib/extensions/__init__.py index 83a522f2..56949b50 100644 --- a/lib/extensions/__init__.py +++ b/lib/extensions/__init__.py @@ -5,32 +5,42 @@ from lib.extensions.troubleshoot import Troubleshoot +from .apply_threadlist import ApplyThreadlist +from .auto_run import AutoRun from .auto_satin import AutoSatin from .break_apart import BreakApart from .cleanup import Cleanup +from .commands_scale_symbols import CommandsScaleSymbols from .convert_to_satin import ConvertToSatin from .convert_to_stroke import ConvertToStroke from .cut_satin import CutSatin +from .cutwork_segmentation import CutworkSegmentation from .duplicate_params import DuplicateParams from .embroider_settings import EmbroiderSettings from .flip import Flip +from .generate_palette import GeneratePalette from .global_commands import GlobalCommands -from .import_threadlist import ImportThreadlist from .input import Input from .install import Install from .install_custom_palette import InstallCustomPalette from .layer_commands import LayerCommands from .lettering import Lettering 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 .letters_to_font import LettersToFont from .object_commands import ObjectCommands +from .object_commands_toggle_visibility import ObjectCommandsToggleVisibility from .output import Output +from .palette_split_text import PaletteSplitText +from .palette_to_text import PaletteToText from .params import Params from .print_pdf import Print from .remove_embroidery_settings import RemoveEmbroiderySettings from .reorder import Reorder from .selection_to_pattern import SelectionToPattern +from .selection_to_guide_line import SelectionToGuideLine from .simulator import Simulator from .stitch_plan_preview import StitchPlanPreview from .zip import Zip @@ -44,24 +54,34 @@ __all__ = extensions = [StitchPlanPreview, Zip, Flip, SelectionToPattern, + SelectionToGuideLine, ObjectCommands, + ObjectCommandsToggleVisibility, LayerCommands, GlobalCommands, + CommandsScaleSymbols, ConvertToSatin, ConvertToStroke, CutSatin, AutoSatin, + AutoRun, Lettering, LetteringGenerateJson, LetteringRemoveKerning, LetteringCustomFontDir, + LetteringForceLockStitches, + LettersToFont, Troubleshoot, RemoveEmbroiderySettings, Cleanup, BreakApart, - ImportThreadlist, + ApplyThreadlist, InstallCustomPalette, + GeneratePalette, + PaletteSplitText, + PaletteToText, Simulator, Reorder, DuplicateParams, - EmbroiderSettings] + EmbroiderSettings, + CutworkSegmentation] diff --git a/lib/extensions/import_threadlist.py b/lib/extensions/apply_threadlist.py index f7fe0bcc..31861513 100644 --- a/lib/extensions/import_threadlist.py +++ b/lib/extensions/apply_threadlist.py @@ -14,7 +14,12 @@ from ..threads import ThreadCatalog from .base import InkstitchExtension -class ImportThreadlist(InkstitchExtension): +class ApplyThreadlist(InkstitchExtension): + ''' + Applies colors of a thread list to elements + Count of colors and elements should fit together + Use case: reapply colors to e.g. a dst file + ''' def __init__(self, *args, **kwargs): InkstitchExtension.__init__(self, *args, **kwargs) self.arg_parser.add_argument("-f", "--filepath", type=str, default="", dest="filepath") @@ -23,7 +28,7 @@ class ImportThreadlist(InkstitchExtension): def effect(self): # Remove selection, we want all the elements in the document - self.svg.selected.clear() + self.svg.selection.clear() if not self.get_elements(): return diff --git a/lib/extensions/auto_run.py b/lib/extensions/auto_run.py new file mode 100644 index 00000000..02997fd0 --- /dev/null +++ b/lib/extensions/auto_run.py @@ -0,0 +1,65 @@ +# 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 inkex + +from ..elements import Stroke +from ..i18n import _ +from ..stitches.auto_run import autorun +from .commands import CommandsExtension + + +class AutoRun(CommandsExtension): + COMMANDS = ["trim"] + + def __init__(self, *args, **kwargs): + CommandsExtension.__init__(self, *args, **kwargs) + + self.arg_parser.add_argument("-b", "--break_up", dest="break_up", type=inkex.Boolean, default=True) + self.arg_parser.add_argument("-p", "--preserve_order", dest="preserve_order", type=inkex.Boolean, default=False) + self.arg_parser.add_argument("-o", "--options", dest="options", type=str, default="") + self.arg_parser.add_argument("-i", "--info", dest="help", type=str, default="") + + def effect(self): + elements = self.check_selection() + if not elements: + return + + starting_point = self.get_starting_point() + ending_point = self.get_ending_point() + + break_up = self.options.break_up + + autorun(elements, self.options.preserve_order, break_up, starting_point, ending_point, self.options.trim) + + def get_starting_point(self): + return self.get_command_point("run_start") + + def get_ending_point(self): + return self.get_command_point("run_end") + + def get_command_point(self, command_type): + command = None + for stroke in self.elements: + command = stroke.get_command(command_type) + # return the first occurence directly + if command: + return command.target_point + + def check_selection(self): + if not self.get_elements(): + return + + if not self.svg.selection: + # L10N auto-route running stitch columns extension + inkex.errormsg(_("Please select one or more stroke elements.")) + return False + + elements = [element for element in self.elements if isinstance(element, Stroke)] + if len(elements) == 0: + inkex.errormsg(_("Please select at least one stroke element.")) + return False + + return elements diff --git a/lib/extensions/auto_satin.py b/lib/extensions/auto_satin.py index 62fb15af..dfb1a87e 100644 --- a/lib/extensions/auto_satin.py +++ b/lib/extensions/auto_satin.py @@ -44,7 +44,7 @@ class AutoSatin(CommandsExtension): if not self.get_elements(): return - if not self.svg.selected: + if not self.svg.selection: # L10N auto-route satin columns extension inkex.errormsg(_("Please select one or more satin columns.")) return False diff --git a/lib/extensions/base.py b/lib/extensions/base.py index 828e3685..cf94714c 100644 --- a/lib/extensions/base.py +++ b/lib/extensions/base.py @@ -8,18 +8,21 @@ import os import re from collections.abc import MutableMapping -import inkex from lxml import etree +from lxml.etree import Comment from stringcase import snakecase +import inkex + from ..commands import is_command, layer_commands from ..elements import EmbroideryElement, nodes_to_elements from ..elements.clone import is_clone from ..i18n import _ -from ..patterns import is_pattern +from ..marker import has_marker from ..svg import generate_unique_id from ..svg.tags import (CONNECTOR_TYPE, EMBROIDERABLE_TAGS, INKSCAPE_GROUPMODE, - NOT_EMBROIDERABLE_TAGS, SVG_DEFS_TAG, SVG_GROUP_TAG) + NOT_EMBROIDERABLE_TAGS, SVG_CLIPPATH_TAG, SVG_DEFS_TAG, + SVG_GROUP_TAG, SVG_MASK_TAG) SVG_METADATA_TAG = inkex.addNS("metadata", "svg") @@ -119,7 +122,7 @@ class InkstitchExtension(inkex.Effect): return current_layer def no_elements_error(self): - if self.svg.selected: + if self.svg.selection: # l10n This was previously: "No embroiderable paths selected." inkex.errormsg(_("Ink/Stitch doesn't know how to work with any of the objects you've selected.") + "\n") else: @@ -129,6 +132,10 @@ class InkstitchExtension(inkex.Effect): def descendants(self, node, selected=False, troubleshoot=False): # noqa: C901 nodes = [] + + if node.tag == Comment: + return [] + element = EmbroideryElement(node) if element.has_command('ignore_object'): @@ -141,15 +148,17 @@ class InkstitchExtension(inkex.Effect): if (node.tag in EMBROIDERABLE_TAGS or node.tag == SVG_GROUP_TAG) and element.get_style('display', 'inline') is None: return [] - if node.tag == SVG_DEFS_TAG: + # defs, masks and clippaths can contain embroiderable elements + # but should never be rendered directly. + if node.tag in [SVG_DEFS_TAG, SVG_MASK_TAG, SVG_CLIPPATH_TAG]: return [] # command connectors with a fill color set, will glitch into the elements list if is_command(node) or node.get(CONNECTOR_TYPE): return [] - if self.svg.selected: - if node.get("id") in self.svg.selected: + if self.svg.selection: + if node.get("id") in self.svg.selection: selected = True else: # if the user didn't select anything that means we process everything @@ -161,10 +170,10 @@ class InkstitchExtension(inkex.Effect): if selected: if node.tag == SVG_GROUP_TAG: pass - elif (node.tag in EMBROIDERABLE_TAGS or is_clone(node)) and not is_pattern(node): + elif (node.tag in EMBROIDERABLE_TAGS or is_clone(node)) and not has_marker(node): nodes.append(node) - # add images, text and patterns for the troubleshoot extension - elif troubleshoot and (node.tag in NOT_EMBROIDERABLE_TAGS or is_pattern(node)): + # add images, text and elements with a marker for the troubleshoot extension + elif troubleshoot and (node.tag in NOT_EMBROIDERABLE_TAGS or has_marker(node)): nodes.append(node) return nodes @@ -180,14 +189,6 @@ class InkstitchExtension(inkex.Effect): self.no_elements_error() return False - def get_selected_in_order(self): - selected = [] - for i in self.options.ids: - path = '//*[@id="%s"]' % i - for node in self.document.xpath(path, namespaces=inkex.NSS): - selected.append(node) - return selected - def elements_to_stitch_groups(self, elements): patches = [] for element in elements: diff --git a/lib/extensions/break_apart.py b/lib/extensions/break_apart.py index b16c901d..581e49bc 100644 --- a/lib/extensions/break_apart.py +++ b/lib/extensions/break_apart.py @@ -27,7 +27,7 @@ class BreakApart(InkstitchExtension): self.minimum_size = 5 def effect(self): # noqa: C901 - if not self.svg.selected: + if not self.svg.selection: inkex.errormsg(_("Please select one or more fill areas to break apart.")) return @@ -83,7 +83,7 @@ class BreakApart(InkstitchExtension): if diff.geom_type == 'MultiPolygon': polygons.remove(other) polygons.remove(polygon) - for p in diff: + for p in diff.geoms: polygons.append(p) # it is possible, that a polygons overlap with multiple # polygons, this means, we need to start all over again diff --git a/lib/extensions/cleanup.py b/lib/extensions/cleanup.py index 99b72a81..4c350d62 100644 --- a/lib/extensions/cleanup.py +++ b/lib/extensions/cleanup.py @@ -5,7 +5,7 @@ from inkex import NSS, Boolean, errormsg -from ..elements import Fill, Stroke +from ..elements import FillStitch, Stroke from ..i18n import _ from .base import InkstitchExtension @@ -24,7 +24,7 @@ class Cleanup(InkstitchExtension): self.fill_threshold = self.options.fill_threshold self.stroke_threshold = self.options.stroke_threshold - self.svg.selected.clear() + self.svg.selection.clear() count = 0 svg = self.document.getroot() @@ -38,7 +38,7 @@ class Cleanup(InkstitchExtension): return for element in self.elements: - if (isinstance(element, Fill) and self.rm_fill and element.shape.area < self.fill_threshold): + if (isinstance(element, FillStitch) and self.rm_fill and element.shape.area < self.fill_threshold): element.node.getparent().remove(element.node) count += 1 if (isinstance(element, Stroke) and self.rm_stroke and diff --git a/lib/extensions/commands_scale_symbols.py b/lib/extensions/commands_scale_symbols.py new file mode 100644 index 00000000..2e025000 --- /dev/null +++ b/lib/extensions/commands_scale_symbols.py @@ -0,0 +1,23 @@ +# Authors: see git history +# +# Copyright (c) 2010 Authors +# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. + +from inkex import NSS, Transform + +from .base import InkstitchExtension + + +class CommandsScaleSymbols(InkstitchExtension): + def __init__(self, *args, **kwargs): + InkstitchExtension.__init__(self, *args, **kwargs) + self.arg_parser.add_argument("-s", "--size", dest="size", type=float, default=1) + + def effect(self): + size = self.options.size + + svg = self.document.getroot() + command_symbols = svg.xpath(".//svg:symbol[starts-with(@id,'inkstitch_')]", namespaces=NSS) + for symbol in command_symbols: + transform = Transform(symbol.get('transform')).add_scale(size) + symbol.set('transform', str(transform)) diff --git a/lib/extensions/convert_to_satin.py b/lib/extensions/convert_to_satin.py index f3b43366..9950b2e8 100644 --- a/lib/extensions/convert_to_satin.py +++ b/lib/extensions/convert_to_satin.py @@ -31,7 +31,7 @@ class ConvertToSatin(InkstitchExtension): if not self.get_elements(): return - if not self.svg.selected: + if not self.svg.selection: inkex.errormsg(_("Please select at least one line to convert to a satin column.")) return @@ -102,7 +102,8 @@ class ConvertToSatin(InkstitchExtension): """Convert svg line join style to shapely parallel offset arguments.""" args = { - 'join_style': shgeo.JOIN_STYLE.round + # mitre is the default per SVG spec + 'join_style': shgeo.JOIN_STYLE.mitre } element_join_style = element.get_style('stroke-linejoin') @@ -116,6 +117,8 @@ class ConvertToSatin(InkstitchExtension): args['mitre_limit'] = miter_limit elif element_join_style == "bevel": args['join_style'] = shgeo.JOIN_STYLE.bevel + elif element_join_style == "round": + args['join_style'] = shgeo.JOIN_STYLE.round return args diff --git a/lib/extensions/convert_to_stroke.py b/lib/extensions/convert_to_stroke.py index dfaef615..5a2ab23c 100644 --- a/lib/extensions/convert_to_stroke.py +++ b/lib/extensions/convert_to_stroke.py @@ -21,7 +21,7 @@ class ConvertToStroke(InkstitchExtension): self.arg_parser.add_argument("-k", "--keep_satin", type=inkex.Boolean, default=False, dest="keep_satin") def effect(self): - if not self.svg.selected or not self.get_elements(): + if not self.svg.selection or not self.get_elements(): inkex.errormsg(_("Please select at least one satin column to convert to a running stitch.")) return diff --git a/lib/extensions/cut_satin.py b/lib/extensions/cut_satin.py index fcd1ca06..3d38c7d8 100644 --- a/lib/extensions/cut_satin.py +++ b/lib/extensions/cut_satin.py @@ -16,7 +16,7 @@ class CutSatin(InkstitchExtension): if not self.get_elements(): return - if not self.svg.selected: + if not self.svg.selection: inkex.errormsg(_("Please select one or more satin columns to cut.")) return diff --git a/lib/extensions/cutwork_segmentation.py b/lib/extensions/cutwork_segmentation.py new file mode 100644 index 00000000..672aeade --- /dev/null +++ b/lib/extensions/cutwork_segmentation.py @@ -0,0 +1,191 @@ +# Authors: see git history +# +# Copyright (c) 2022 Authors +# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. + +from math import atan2, degrees + +from lxml import etree +from shapely.geometry import LineString, Point + +import inkex + +from ..elements import Stroke +from ..i18n import _ +from ..svg import get_correction_transform +from ..svg.tags import INKSCAPE_LABEL, INKSTITCH_ATTRIBS, SVG_PATH_TAG +from .base import InkstitchExtension + + +class CutworkSegmentation(InkstitchExtension): + ''' + This will split up stroke elements according to their direction. + Overlapping angle definitions (user input) will result in overlapping paths. + This is wanted behaviour if the needles have a hard time to cut edges at the border of their specific angle capability. + ''' + def __init__(self, *args, **kwargs): + InkstitchExtension.__init__(self, *args, **kwargs) + self.arg_parser.add_argument("-o", "--options", type=str, default=None, dest="page_1") + self.arg_parser.add_argument("-i", "--info", type=str, default=None, dest="page_2") + self.arg_parser.add_argument("-as", "--a_start", type=int, default=0, dest="a_start") + self.arg_parser.add_argument("-ae", "--a_end", type=int, default=0, dest="a_end") + self.arg_parser.add_argument("-ac", "--a_color", type=inkex.Color, default=inkex.Color(0x808080FF), dest="a_color") + self.arg_parser.add_argument("-bs", "--b_start", type=int, default=0, dest="b_start") + self.arg_parser.add_argument("-be", "--b_end", type=int, default=0, dest="b_end") + self.arg_parser.add_argument("-bc", "--b_color", type=inkex.Color, default=inkex.Color(0x808080FF), dest="b_color") + self.arg_parser.add_argument("-cs", "--c_start", type=int, default=0, dest="c_start") + self.arg_parser.add_argument("-ce", "--c_end", type=int, default=0, dest="c_end") + self.arg_parser.add_argument("-cc", "--c_color", type=inkex.Color, default=inkex.Color(0x808080FF), dest="c_color") + self.arg_parser.add_argument("-ds", "--d_start", type=int, default=0, dest="d_start") + self.arg_parser.add_argument("-de", "--d_end", type=int, default=0, dest="d_end") + self.arg_parser.add_argument("-dc", "--d_color", type=inkex.Color, default=inkex.Color(0x808080FF), dest="d_color") + self.arg_parser.add_argument("-s", "--sort_by_color", type=inkex.Boolean, default=True, dest="sort_by_color") + self.arg_parser.add_argument("-k", "--keep_original", type=inkex.Boolean, default=False, dest="keep_original") + + def effect(self): + if not self.svg.selection: + inkex.errormsg(_("Please select one or more stroke elements.")) + return + + if not self.get_elements(): + return + + self.sectors = { + 1: {'id': 1, 'start': self.options.a_start, 'end': self.options.a_end, 'color': self.options.a_color, 'point_list': []}, + 2: {'id': 2, 'start': self.options.b_start, 'end': self.options.b_end, 'color': self.options.b_color, 'point_list': []}, + 3: {'id': 3, 'start': self.options.c_start, 'end': self.options.c_end, 'color': self.options.c_color, 'point_list': []}, + 4: {'id': 4, 'start': self.options.d_start, 'end': self.options.d_end, 'color': self.options.d_color, 'point_list': []} + } + + # remove sectors where the start angle equals the end angle (some setups only work with two needles instead of four) + self.sectors = {index: sector for index, sector in self.sectors.items() if sector['start'] != sector['end']} + + self.new_elements = [] + for element in self.elements: + if isinstance(element, Stroke): + + # save parent and index to be able to position and insert new elements later on + parent = element.node.getparent() + index = parent.index(element.node) + + for path in element.paths: + linestring = LineString(path) + # fill self.new_elements list with line segments + self._prepare_line_sections(element, linestring.coords) + + self._insert_elements(parent, element, index) + + self._remove_originals() + + def _get_sectors(self, angle): + sectors = [] + for sector in self.sectors.values(): + if self._in_sector(angle, sector): + sectors.append(sector) + return sectors + + def _in_sector(self, angle, sector): + stop = sector['end'] + 1 + if sector['start'] > stop: + return angle in range(sector['start'], 181) or angle in range(0, stop) + else: + return angle in range(sector['start'], stop) + + def _get_angle(self, p1, p2): + angle = round(degrees(atan2(p2.y - p1.y, p2.x - p1.x)) % 360) + if angle > 180: + angle -= 180 + return angle + + def _prepare_line_sections(self, element, coords): + prev_point = None + current_sectors = [] + + for index, point in enumerate(coords): + point = Point(*point) + if prev_point is None: + prev_point = point + continue + + angle = self._get_angle(point, prev_point) + sectors = self._get_sectors(angle) + + for sector in sectors: + self.sectors[sector['id']]['point_list'].append(prev_point) + # don't miss the last point + if index == len(coords) - 1: + self.sectors[sector['id']]['point_list'].append(point) + self._prepare_element(self.sectors[sector['id']], element) + + # if a segment ends, prepare the element and clear point_lists + for current in current_sectors: + if current not in sectors: + # add last point + self.sectors[current['id']]['point_list'].append(prev_point) + self._prepare_element(self.sectors[current['id']], element) + + prev_point = point + current_sectors = sectors + + def _prepare_element(self, sector, element): + point_list = sector['point_list'] + if len(point_list) < 2: + return + + color = str(self.path_style(element, str(sector['color']))) + + d = "M " + for point in point_list: + d += "%s,%s " % (point.x, point.y) + + stroke_element = etree.Element(SVG_PATH_TAG, + { + "style": color, + "transform": get_correction_transform(element.node), + INKSTITCH_ATTRIBS["ties"]: "3", + INKSTITCH_ATTRIBS["running_stitch_length_mm"]: "1", + "d": d + }) + self.new_elements.append([stroke_element, sector['id']]) + # clear point_list in self.sectors + self.sectors[sector['id']].update({'point_list': []}) + + def _insert_elements(self, parent, element, index): + self.new_elements.reverse() + if self.options.sort_by_color is True: + self.new_elements = sorted(self.new_elements, key=lambda x: x[1], reverse=True) + + group = self._insert_group(parent, _("Cutwork Group"), "__inkstitch_cutwork_group__", index) + + section = 0 + for element, section_id in self.new_elements: + # if sorted by color, add a subgroup for each knife + if self.options.sort_by_color: + if section_id != section: + section = section_id + section_group = self._insert_group(group, _("Needle #%s") % section, "__inkstitch_cutwork_needle_group__") + else: + section_group = group + + section_group.insert(0, element) + + def _insert_group(self, parent, label, group_id, index=0): + group = etree.Element("g", { + INKSCAPE_LABEL: "%s" % label, + "id": self.uniqueId("%s" % group_id) + }) + parent.insert(index, group) + return group + + def _remove_originals(self): + if self.options.keep_original: + return + + for element in self.elements: + if isinstance(element, Stroke): + parent = element.node.getparent() + parent.remove(element.node) + + def path_style(self, element, color): + # set stroke color and make it a running stitch - they don't want to cut zigzags + return inkex.Style(element.node.get('style', '')) + inkex.Style('stroke:%s;stroke-dasharray:6,1;' % color) diff --git a/lib/extensions/duplicate_params.py b/lib/extensions/duplicate_params.py index 9fcdbf1c..46110691 100644 --- a/lib/extensions/duplicate_params.py +++ b/lib/extensions/duplicate_params.py @@ -3,10 +3,9 @@ # Copyright (c) 2021 Authors # Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. -import inkex +from inkex import NSS, ShapeElement, errormsg from ..i18n import _ -from ..svg.tags import EMBROIDERABLE_TAGS, SVG_GROUP_TAG from .base import InkstitchExtension @@ -14,36 +13,29 @@ class DuplicateParams(InkstitchExtension): # Transfer inkstitch namespaced attributes from the first selected element to the rest of selection def effect(self): - objects = self.get_selected_in_order() + objects = self.svg.selection.get(ShapeElement) if len(objects) < 2: - inkex.errormsg(_("This function copies Ink/Stitch parameters from the first selected element to the rest of the selection. " - "Please select at least two elements.")) + errormsg(_("This function copies Ink/Stitch parameters from the first selected element to the rest of the selection. " + "Please select at least two elements.")) return - copy_from = objects[0] + copy_from = objects.first() copy_from_attribs = self.get_inkstitch_attributes(copy_from) - copy_to_selection = objects[1:] - self.copy_to = [] - - # extract copy_to group elements - for element in copy_to_selection: - if element.tag == SVG_GROUP_TAG: - for descendant in element.iterdescendants(EMBROIDERABLE_TAGS): - self.copy_to.append(descendant) - elif element.tag in EMBROIDERABLE_TAGS: - self.copy_to.append(element) + copy_to = objects # remove inkstitch params from copy_to elements - for element in self.copy_to: + for element in copy_to: + if element == copy_to.first(): + continue copy_to_attribs = self.get_inkstitch_attributes(element) for attrib in copy_to_attribs: element.pop(attrib) # paste inkstitch params from copy_from element to copy_to elements for attrib in copy_from_attribs: - for element in self.copy_to: + for element in copy_to: element.attrib[attrib] = copy_from_attribs[attrib] def get_inkstitch_attributes(self, node): - return {k: v for k, v in node.attrib.iteritems() if inkex.NSS['inkstitch'] in k} + return {k: v for k, v in node.attrib.iteritems() if NSS['inkstitch'] in k} diff --git a/lib/extensions/flip.py b/lib/extensions/flip.py index 743f1701..893dc038 100644 --- a/lib/extensions/flip.py +++ b/lib/extensions/flip.py @@ -24,7 +24,7 @@ class Flip(InkstitchExtension): if not self.get_elements(): return - if not self.svg.selected: + if not self.svg.selection: inkex.errormsg(_("Please select one or more satin columns to flip.")) return diff --git a/lib/extensions/generate_palette.py b/lib/extensions/generate_palette.py new file mode 100644 index 00000000..280be90f --- /dev/null +++ b/lib/extensions/generate_palette.py @@ -0,0 +1,84 @@ +# Authors: see git history +# +# Copyright (c) 2022 Authors +# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. + +import os + +import inkex + +from ..i18n import _ +from ..utils import guess_inkscape_config_path +from .base import InkstitchExtension + + +class GeneratePalette(InkstitchExtension): + # Generate a custom color palette in object related order + def __init__(self, *args, **kwargs): + InkstitchExtension.__init__(self, *args, **kwargs) + self.arg_parser.add_argument("-n", "--palette_name", type=str, default=None, dest="palette_name") + self.arg_parser.add_argument("-f", "--palette_folder", type=str, default=None, dest="palette_folder") + self.arg_parser.add_argument("-o", "--options", type=str, default=None, dest="page_options") + self.arg_parser.add_argument("-i", "--info", type=str, default=None, dest="page_help") + + def effect(self): + path = self.options.palette_folder + brand = self.options.palette_name + file_name = "InkStitch %s.gpl" % brand + color_palette_name = '\nName: Ink/Stitch: %s' % brand + + if not brand: + inkex.errormsg(_("Please specify a name for your color palette.")) + return + + if path: + if not os.path.isdir(path): + inkex.errormsg(_("Unkown directory path.")) + return + else: + path = os.path.join(guess_inkscape_config_path(), 'palettes') + if not os.path.isdir(path): + inkex.errormsg(_("Ink/Stitch cannot find your palette folder automatically. Please enter the path manually.")) + return + + elements = self.svg.selection.rendering_order() + + if not elements: + inkex.errormsg(_("No element selected.\n\nPlease select at least one text element with a fill color.")) + return + + colors = self._get_color_from_elements(elements) + + if not colors: + inkex.errormsg(_("We couldn't find any fill colors on your text elements. Please read the instructions on our website.")) + return + + colors = ['GIMP Palette', color_palette_name, '\nColumns: 4', '\n# RGB Value\t Color Name Number'] + colors + + file_path = os.path.join(path, file_name) + with open(file_path, 'w', encoding='utf-8') as gpl: + gpl.writelines(colors) + + def _get_color_from_elements(self, elements): + colors = [] + for element in elements: + if 'fill' not in element.style.keys() or type(element) != inkex.TextElement: + continue + + color = inkex.Color(element.style['fill']).to_rgb() + color_name = element.get_text().split(' ') + if len(color_name) > 1 and color_name[-1].isdigit(): + number = color_name[-1] + name = ' '.join(color_name[:-1]) + else: + number = 0 + name = ' '.join(color_name) + color = "\n%s\t%s\t%s\t%s %s" % (str(color[0]).rjust(3), str(color[1]).rjust(3), str(color[2]).rjust(3), name.rjust(30), number) + colors.append(color) + + return colors + + +if __name__ == '__main__': + e = GeneratePalette() + e.affect() diff --git a/lib/extensions/input.py b/lib/extensions/input.py index a8b8bee3..066b9003 100644 --- a/lib/extensions/input.py +++ b/lib/extensions/input.py @@ -3,70 +3,13 @@ # Copyright (c) 2010 Authors # Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. -import os -import sys - -import inkex from lxml import etree -import pyembroidery - -from ..i18n import _ -from ..stitch_plan import StitchPlan -from ..svg import PIXELS_PER_MM, render_stitch_plan -from ..svg.tags import INKSCAPE_LABEL +from ..stitch_plan import generate_stitch_plan class Input(object): def run(self, args): embroidery_file = args[0] - self.validate_file_path(embroidery_file) - - pattern = pyembroidery.read(embroidery_file) - stitch_plan = StitchPlan() - color_block = None - - for raw_stitches, thread in pattern.get_as_colorblocks(): - color_block = stitch_plan.new_color_block(thread) - for x, y, command in raw_stitches: - if command == pyembroidery.STITCH: - color_block.add_stitch(x * PIXELS_PER_MM / 10.0, y * PIXELS_PER_MM / 10.0) - if len(color_block) > 0: - if command == pyembroidery.TRIM: - color_block.add_stitch(trim=True) - elif command == pyembroidery.STOP: - color_block.add_stitch(stop=True) - color_block = stitch_plan.new_color_block(thread) - - stitch_plan.delete_empty_color_blocks() - - if stitch_plan.last_color_block: - if stitch_plan.last_color_block.last_stitch: - if stitch_plan.last_color_block.last_stitch.stop: - # ending with a STOP command is redundant, so remove it - del stitch_plan.last_color_block[-1] - - extents = stitch_plan.extents - svg = inkex.SvgDocumentElement("svg", nsmap=inkex.NSS, attrib={ - "width": str(extents[0] * 2), - "height": str(extents[1] * 2), - "viewBox": "0 0 %s %s" % (extents[0] * 2, extents[1] * 2), - }) - render_stitch_plan(svg, stitch_plan) - - # rename the Stitch Plan layer so that it doesn't get overwritten by Embroider - layer = svg.find(".//*[@id='__inkstitch_stitch_plan__']") - layer.set(INKSCAPE_LABEL, os.path.basename(embroidery_file)) - layer.attrib.pop('id') - - # Shift the design so that its origin is at the center of the canvas - # Note: this is NOT the same as centering the design in the canvas! - layer.set('transform', 'translate(%s,%s)' % (extents[0], extents[1])) - - print(etree.tostring(svg).decode('utf-8')) - - def validate_file_path(self, path): - # Check if the file exists - if not os.path.isfile(path): - inkex.errormsg(_('File does not exist and cannot be opened. Please correct the file path and try again.\r%s') % path) - sys.exit(1) + stitch_plan = generate_stitch_plan(embroidery_file) + print(etree.tostring(stitch_plan).decode('utf-8')) diff --git a/lib/extensions/lettering.py b/lib/extensions/lettering.py index 312a47ce..658f2bc7 100644 --- a/lib/extensions/lettering.py +++ b/lib/extensions/lettering.py @@ -9,10 +9,11 @@ import sys from base64 import b64decode import appdirs -import inkex import wx import wx.adv +import inkex + from ..elements import nodes_to_elements from ..gui import PresetsPanel, SimulatorPreview, info_dialog from ..i18n import _ @@ -174,7 +175,7 @@ class LetteringFrame(wx.Frame): image.Rescale(300, 20, quality=wx.IMAGE_QUALITY_HIGH) self.font_chooser.Append(font.marked_custom_font_name, wx.Bitmap(image)) else: - self.font_chooser.Append(font.name) + self.font_chooser.Append(font.marked_custom_font_name) def get_font_descriptions(self): return {font.name: font.description for font in self.fonts.values()} @@ -272,12 +273,15 @@ class LetteringFrame(wx.Frame): font.render_text(self.settings.text, destination_group, back_and_forth=self.settings.back_and_forth, trim=self.settings.trim) except FontError as e: if raise_error: - inkex.errormsg("Error: Text cannot be applied to the document.\n%s" % e) + inkex.errormsg(_("Error: Text cannot be applied to the document.\n%s") % e) return else: pass - if self.settings.scale != 100: + # destination_group isn't always the text scaling group (but also the parent group) + # the text scaling group label is dependend on the user language, so it would break in international file exchange if we used it + # scaling (correction transform) on the parent group is already applied, so let's use that for recognition + if self.settings.scale != 100 and not destination_group.get('transform', None): destination_group.attrib['transform'] = 'scale(%s)' % (self.settings.scale / 100.0) def generate_patches(self, abort_early=None): @@ -395,10 +399,10 @@ class Lettering(CommandsExtension): self.cancelled = True def get_or_create_group(self): - if self.svg.selected: + if self.svg.selection: groups = set() - for node in self.svg.selected.values(): + for node in self.svg.selection: if node.tag == SVG_GROUP_TAG and INKSTITCH_LETTERING in node.attrib: groups.add(node) diff --git a/lib/extensions/lettering_force_lock_stitches.py b/lib/extensions/lettering_force_lock_stitches.py new file mode 100644 index 00000000..62d7ae14 --- /dev/null +++ b/lib/extensions/lettering_force_lock_stitches.py @@ -0,0 +1,86 @@ +# Authors: see git history +# +# Copyright (c) 2021 Authors +# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. + +import inkex +from shapely.geometry import Point + +from ..i18n import _ +from ..svg import PIXELS_PER_MM +from ..svg.tags import INKSTITCH_ATTRIBS +from .base import InkstitchExtension + + +class LetteringForceLockStitches(InkstitchExtension): + ''' + This extension helps font creators to add the force lock stitches attribute to the last objects of each glyph + Font creators to add forced lock stitches on glyphs with accents / spaces. + ''' + + def __init__(self, *args, **kwargs): + InkstitchExtension.__init__(self, *args, **kwargs) + self.arg_parser.add_argument("-a", "--max_distance", type=float, default=3, dest="max_distance") + self.arg_parser.add_argument("-i", "--min_distance", type=float, default=1, dest="min_distance") + self.arg_parser.add_argument("-l", "--last_element", type=inkex.Boolean, dest="last_element") + + def effect(self): + if self.options.max_distance < self.options.min_distance: + inkex.errormssg(_("The maximum value is smaller than the minimum value.")) + + # Set glyph layers to be visible. We don't want them to be ignored by self.elements + self._update_layer_visibility('inline') + + # mark last elements of a glyph + xpath = ".//svg:g[@inkscape:groupmode='layer']//svg:path[last()]" + last_elements = self.document.xpath(xpath, namespaces=inkex.NSS) + for last_element in last_elements: + last_element.set('lastglyphelement', str(True)) + + # find last point of an element + if not self.get_elements(): + return + + previous_element = None + last_stitch = None + for element in self.elements: + stitch_group = element.to_stitch_groups(None) + # if the distance of the last stitch of the previous object to the first stitch of this objects + # lies within the user defined distance range, set the force_lock_stitches-attribute. + if last_stitch: + first_stitch = stitch_group[0].stitches[0] + first_stitch = Point(first_stitch.x, first_stitch.y) + self._set_force_attribute(first_stitch, last_stitch, previous_element) + + # if this is the last element of a glyph, we don't want to compare it to the next element + if element.node.get('lastglyphelement', False): + previous_element = None + last_stitch = None + else: + previous_element = element + last_stitch = stitch_group[-1].stitches[-1] + last_stitch = Point(last_stitch.x, last_stitch.y) + + # remove last element attributes again + # set force lock stitches attribute if needed + for last_element in last_elements: + last_element.attrib.pop('lastglyphelement') + if self.options.last_element: + last_element.set(INKSTITCH_ATTRIBS['force_lock_stitches'], True) + + # hide glyph layers again + self._update_layer_visibility('none') + + def _set_force_attribute(self, first_stitch, last_stitch, previous_element): + distance_mm = first_stitch.distance(last_stitch) / PIXELS_PER_MM + + if distance_mm < self.options.max_distance and distance_mm > self.options.min_distance: + previous_element.node.set(INKSTITCH_ATTRIBS['force_lock_stitches'], True) + + def _update_layer_visibility(self, display): + xpath = ".//svg:g[@inkscape:groupmode='layer']" + layers = self.document.xpath(xpath, namespaces=inkex.NSS) + for layer in layers: + display_style = 'display:%s' % display + style = inkex.Style(layer.get('style', '')) + inkex.Style(display_style) + layer.set('style', style) diff --git a/lib/extensions/letters_to_font.py b/lib/extensions/letters_to_font.py new file mode 100644 index 00000000..158d0d9f --- /dev/null +++ b/lib/extensions/letters_to_font.py @@ -0,0 +1,81 @@ +# Authors: see git history +# +# Copyright (c) 2021 Authors +# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. + +import os +from pathlib import Path + +import inkex +from inkex import errormsg + +from ..commands import ensure_symbol +from ..i18n import _ +from ..stitch_plan import generate_stitch_plan +from ..svg import get_correction_transform +from ..svg.tags import INKSCAPE_GROUPMODE, INKSCAPE_LABEL, SVG_PATH_TAG +from .base import InkstitchExtension + + +class LettersToFont(InkstitchExtension): + ''' + This extension will create a json file to store a custom directory path for additional user fonts + ''' + def __init__(self, *args, **kwargs): + InkstitchExtension.__init__(self, *args, **kwargs) + self.arg_parser.add_argument("-d", "--font-dir", type=str, default="", dest="font_dir") + self.arg_parser.add_argument("-f", "--file-format", type=str, default="", dest="file_format") + self.arg_parser.add_argument("-c", "--import-commands", type=inkex.Boolean, default=False, dest="import_commands") + + def effect(self): + font_dir = self.options.font_dir + file_format = self.options.file_format + + if not os.path.isdir(font_dir): + errormsg(_("Font directory not found. Please specify an existing directory.")) + + glyphs = list(Path(font_dir).rglob(file_format)) + if not glyphs: + glyphs = list(Path(font_dir).rglob(file_format.lower())) + + document = self.document.getroot() + for glyph in glyphs: + letter = self.get_glyph_element(glyph) + label = "GlyphLayer-%s" % letter.get(INKSCAPE_LABEL, ' ').split('.')[0][-1] + group = inkex.Group(attrib={ + INKSCAPE_LABEL: label, + INKSCAPE_GROUPMODE: "layer", + "transform": get_correction_transform(document, child=True) + }) + + # remove color block groups if we import without commands + # there will only be one object per color block anyway + if not self.options.import_commands: + for element in letter.iter(SVG_PATH_TAG): + group.insert(0, element) + else: + group.insert(0, letter) + + document.insert(0, group) + group.set('style', 'display:none') + + # 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') + + self.insert_baseline(document) + + def get_glyph_element(self, glyph): + stitch_plan = generate_stitch_plan(str(glyph), self.options.import_commands) + # we received a stitch plan wrapped in an svg document, we only need the stitch_plan group + # this group carries the name of the file, so we can search for it. + stitch_plan = stitch_plan.xpath('.//*[@inkscape:label="%s"]' % os.path.basename(glyph), namespaces=inkex.NSS)[0] + stitch_plan.attrib.pop(INKSCAPE_GROUPMODE) + return stitch_plan + + def insert_baseline(self, document): + document.namedview.new_guide(position=0.0, name="baseline") diff --git a/lib/extensions/object_commands.py b/lib/extensions/object_commands.py index 851d4a34..4d692cae 100644 --- a/lib/extensions/object_commands.py +++ b/lib/extensions/object_commands.py @@ -17,7 +17,7 @@ class ObjectCommands(CommandsExtension): if not self.get_elements(): return - if not self.svg.selected: + if not self.svg.selection: inkex.errormsg(_("Please select one or more objects to which to attach commands.")) return @@ -34,6 +34,6 @@ class ObjectCommands(CommandsExtension): seen_nodes = set() for element in self.elements: - if element.node not in seen_nodes: + if element.node not in seen_nodes and element.shape: add_commands(element, commands) seen_nodes.add(element.node) diff --git a/lib/extensions/object_commands_toggle_visibility.py b/lib/extensions/object_commands_toggle_visibility.py new file mode 100644 index 00000000..569f4305 --- /dev/null +++ b/lib/extensions/object_commands_toggle_visibility.py @@ -0,0 +1,24 @@ +# Authors: see git history +# +# Copyright (c) 2022 Authors +# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. + +from inkex import NSS + +from .base import InkstitchExtension + + +class ObjectCommandsToggleVisibility(InkstitchExtension): + + def effect(self): + svg = self.document.getroot() + # toggle object commands (in fact it's display or hide all of them) + command_groups = svg.xpath(".//svg:g[starts-with(@id,'command_group')]", namespaces=NSS) + display = "none" + first_iteration = True + for command_group in command_groups: + if first_iteration: + first_iteration = False + if not command_group.is_visible(): + display = "inline" + command_group.style['display'] = display diff --git a/lib/extensions/palette_split_text.py b/lib/extensions/palette_split_text.py new file mode 100644 index 00000000..3257d694 --- /dev/null +++ b/lib/extensions/palette_split_text.py @@ -0,0 +1,42 @@ +# Authors: see git history +# +# Copyright (c) 2022 Authors +# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. + +import inkex + +from ..i18n import _ +from .base import InkstitchExtension + + +class PaletteSplitText(InkstitchExtension): + # Splits sublines of text into it's own text elements in order to color them with the color picker + def __init__(self, *args, **kwargs): + InkstitchExtension.__init__(self, *args, **kwargs) + self.arg_parser.add_argument("-l", "--line-height", type=int, default=6, dest="line_height") + + def effect(self): + if not self.svg.selection: + inkex.errormsg(_("Please select one or more text elements to split lines.")) + return + + line_height = self.options.line_height + + for text in self.svg.selection.get(inkex.elements.TextElement): + parent = text.getparent() + content = text.get_text() + lines = content.split('\n') + lines.reverse() + style = text.get('style') + x = text.get('x') + y = text.get('y') + y = float(y) + (len(lines) - 1) * line_height + for line in lines: + element = inkex.TextElement() + element.text = line + element.set('style', style) + element.set('x', x) + element.set('y', str(y)) + y = float(y) - line_height + parent.insert(0, element) + parent.remove(text) diff --git a/lib/extensions/palette_to_text.py b/lib/extensions/palette_to_text.py new file mode 100644 index 00000000..0944c649 --- /dev/null +++ b/lib/extensions/palette_to_text.py @@ -0,0 +1,50 @@ +# Authors: see git history +# +# Copyright (c) 2022 Authors +# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. + +import os + +import inkex + +from ..i18n import _ +from ..threads.palette import ThreadPalette +from .base import InkstitchExtension + + +class PaletteToText(InkstitchExtension): + # Generate a custom color palette in object related order + def __init__(self, *args, **kwargs): + InkstitchExtension.__init__(self, *args, **kwargs) + self.arg_parser.add_argument("-f", "--file", type=str, default=None, dest="file") + self.arg_parser.add_argument("-o", "--notebook:options", type=str, default=None, dest="page_options") + self.arg_parser.add_argument("-i", "--info", type=str, default=None, dest="page_help") + + def effect(self): + palette_file = self.options.file + if not os.path.isfile(palette_file): + inkex.errormsg(_("File does not exist.")) + return + + thread_palette = ThreadPalette(palette_file) + current_layer = self.svg.get_current_layer() + + x = 0 + y = 0 + pos = 0 + for color in thread_palette: + line = "%s %s" % (color.name, color.number) + element = inkex.TextElement() + element.text = line + element.style = "fill:%s;font-size:4px;" % color.to_hex_str() + element.set('x', x) + element.set('y', str(y)) + current_layer.insert(pos, element) + + y = float(y) + 5 + pos += 1 + + +if __name__ == '__main__': + e = PaletteToText() + e.affect() diff --git a/lib/extensions/params.py b/lib/extensions/params.py index c96b9691..b60183e5 100644 --- a/lib/extensions/params.py +++ b/lib/extensions/params.py @@ -9,13 +9,13 @@ import os import sys from collections import defaultdict from copy import copy -from itertools import groupby +from itertools import groupby, zip_longest import wx from wx.lib.scrolledpanel import ScrolledPanel from ..commands import is_command, is_command_symbol -from ..elements import (AutoFill, Clone, EmbroideryElement, Fill, Polyline, +from ..elements import (FillStitch, Clone, EmbroideryElement, Polyline, SatinColumn, Stroke) from ..elements.clone import is_clone from ..gui import PresetsPanel, SimulatorPreview, WarningPanel @@ -25,6 +25,11 @@ from ..utils import get_resource_dir from .base import InkstitchExtension +def grouper(iterable_obj, count, fillvalue=None): + args = [iter(iterable_obj)] * count + return zip_longest(*args, fillvalue=fillvalue) + + class ParamsTab(ScrolledPanel): def __init__(self, *args, **kwargs): self.params = kwargs.pop('params', []) @@ -38,6 +43,8 @@ class ParamsTab(ScrolledPanel): self.dependent_tabs = [] self.parent_tab = None self.param_inputs = {} + self.choice_widgets = defaultdict(list) + self.dict_of_choices = {} self.paired_tab = None self.disable_notify_pair = False @@ -46,14 +53,16 @@ class ParamsTab(ScrolledPanel): if toggles: self.toggle = toggles[0] self.params.remove(self.toggle) - self.toggle_checkbox = wx.CheckBox(self, label=self.toggle.description) + self.toggle_checkbox = wx.CheckBox( + self, label=self.toggle.description) value = any(self.toggle.values) if self.toggle.inverse: value = not value self.toggle_checkbox.SetValue(value) - self.toggle_checkbox.Bind(wx.EVT_CHECKBOX, self.update_toggle_state) + self.toggle_checkbox.Bind( + wx.EVT_CHECKBOX, self.update_toggle_state) self.toggle_checkbox.Bind(wx.EVT_CHECKBOX, self.changed) self.param_inputs[self.toggle.name] = self.toggle_checkbox @@ -66,7 +75,8 @@ class ParamsTab(ScrolledPanel): self.settings_grid.AddGrowableCol(1, 2) self.settings_grid.SetFlexibleDirection(wx.HORIZONTAL) - self.pencil_icon = wx.Image(os.path.join(get_resource_dir("icons"), "pencil_20x20.png")).ConvertToBitmap() + self.pencil_icon = wx.Image(os.path.join(get_resource_dir( + "icons"), "pencil_20x20.png")).ConvertToBitmap() self.__set_properties() self.__do_layout() @@ -76,7 +86,6 @@ class ParamsTab(ScrolledPanel): # end wxGlade def pair(self, tab): - # print self.name, "paired with", tab.name self.paired_tab = tab self.update_description() @@ -98,7 +107,6 @@ class ParamsTab(ScrolledPanel): def update_toggle_state(self, event=None, notify_pair=True): enable = self.enabled() - # print self.name, "update_toggle_state", enable for child in self.settings_grid.GetChildren(): widget = child.GetWindow() if widget: @@ -113,8 +121,21 @@ class ParamsTab(ScrolledPanel): if event: event.Skip() + def update_choice_state(self, event=None): + input = event.GetEventObject() + selection = input.GetSelection() + + param = self.inputs_to_params[input] + + self.update_choice_widgets((param, selection)) + self.settings_grid.Layout() + self.Fit() + self.Layout() + + if event: + event.Skip() + def pair_changed(self, value): - # print self.name, "pair_changed", value new_value = not value if self.enabled() != new_value: @@ -169,7 +190,6 @@ class ParamsTab(ScrolledPanel): def apply(self): values = self.get_values() for node in self.nodes: - # print >> sys.stderr, "apply: ", self.name, node.id, values for name, value in values.items(): node.set_param(name, value) @@ -207,19 +227,25 @@ class ParamsTab(ScrolledPanel): if len(self.nodes) == 1: description = _("These settings will be applied to 1 object.") else: - description = _("These settings will be applied to %d objects.") % len(self.nodes) + description = _( + "These settings will be applied to %d objects.") % len(self.nodes) if any(len(param.values) > 1 for param in self.params): - description += "\n • " + _("Some settings had different values across objects. Select a value from the dropdown or enter a new one.") + description += "\n • " + \ + _("Some settings had different values across objects. Select a value from the dropdown or enter a new one.") if self.dependent_tabs: if len(self.dependent_tabs) == 1: - description += "\n • " + _("Disabling this tab will disable the following %d tabs.") % len(self.dependent_tabs) + description += "\n • " + \ + _("Disabling this tab will disable the following %d tabs.") % len( + self.dependent_tabs) else: - description += "\n • " + _("Disabling this tab will disable the following tab.") + description += "\n • " + \ + _("Disabling this tab will disable the following tab.") if self.paired_tab: - description += "\n • " + _("Enabling this tab will disable %s and vice-versa.") % self.paired_tab.name + description += "\n • " + \ + _("Enabling this tab will disable %s and vice-versa.") % self.paired_tab.name self.description_text = description @@ -245,35 +271,70 @@ class ParamsTab(ScrolledPanel): # end wxGlade pass - def __do_layout(self): + # choice tuple is None or contains ("choice widget param name", "actual selection") + def update_choice_widgets(self, choice_tuple=None): + if choice_tuple is None: # update all choices + for choice in self.dict_of_choices.values(): + self.update_choice_widgets( + (choice["param"].name, choice["widget"].GetSelection())) + else: + choice = self.dict_of_choices[choice_tuple[0]] + last_selection = choice["last_initialized_choice"] + current_selection = choice["widget"].GetSelection() + if last_selection != -1 and last_selection != current_selection: # Hide the old widgets + for widget in self.choice_widgets[(choice["param"].name, last_selection)]: + widget.Hide() + # self.settings_grid.Detach(widget) + + for widgets in grouper(self.choice_widgets[choice_tuple], 4): + widgets[0].Show(True) + widgets[1].Show(True) + widgets[2].Show(True) + widgets[3].Show(True) + choice["last_initialized_choice"] = current_selection + + def __do_layout(self, only_settings_grid=False): # noqa: C901 + # just to add space around the settings box = wx.BoxSizer(wx.VERTICAL) - summary_box = wx.StaticBox(self, wx.ID_ANY, label=_("Inkscape objects")) + summary_box = wx.StaticBox( + self, wx.ID_ANY, label=_("Inkscape objects")) sizer = wx.StaticBoxSizer(summary_box, wx.HORIZONTAL) self.description = wx.StaticText(self) self.update_description() self.description.SetLabel(self.description_text) self.description_container = box self.Bind(wx.EVT_SIZE, self.resized) - sizer.Add(self.description, proportion=0, flag=wx.EXPAND | wx.ALL, border=5) + sizer.Add(self.description, proportion=0, + flag=wx.EXPAND | wx.ALL, border=5) box.Add(sizer, proportion=0, flag=wx.ALL, border=5) if self.toggle: toggle_sizer = wx.BoxSizer(wx.HORIZONTAL) - toggle_sizer.Add(self.create_change_indicator(self.toggle.name), proportion=0, flag=wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, border=5) - toggle_sizer.Add(self.toggle_checkbox, proportion=0, flag=wx.ALIGN_CENTER_VERTICAL) + toggle_sizer.Add(self.create_change_indicator( + self.toggle.name), proportion=0, flag=wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, border=5) + toggle_sizer.Add(self.toggle_checkbox, proportion=0, + flag=wx.ALIGN_CENTER_VERTICAL) box.Add(toggle_sizer, proportion=0, flag=wx.BOTTOM, border=10) for param in self.params: - self.settings_grid.Add(self.create_change_indicator(param.name), proportion=0, flag=wx.ALIGN_CENTER_VERTICAL) - + col1 = self.create_change_indicator(param.name) description = wx.StaticText(self, label=param.description) description.SetToolTip(param.tooltip) - self.settings_grid.Add(description, proportion=1, flag=wx.EXPAND | wx.RIGHT | wx.ALIGN_CENTER_VERTICAL | wx.TOP, border=5) - if param.type == 'boolean': + if param.select_items is not None: + col1.Hide() + description.Hide() + for item in param.select_items: + self.choice_widgets[item].extend([col1, description]) + # else: + self.settings_grid.Add( + col1, proportion=0, flag=wx.ALIGN_CENTER_VERTICAL) + self.settings_grid.Add(description, proportion=1, flag=wx.EXPAND | + wx.RIGHT | wx.ALIGN_CENTER_VERTICAL | wx.TOP, border=5) + if param.type == 'boolean': if len(param.values) > 1: input = wx.CheckBox(self, style=wx.CHK_3STATE) input.Set3StateValue(wx.CHK_UNDETERMINED) @@ -287,8 +348,12 @@ class ParamsTab(ScrolledPanel): input = wx.Choice(self, wx.ID_ANY, choices=param.options) input.SetSelection(int(param.values[0])) input.Bind(wx.EVT_CHOICE, self.changed) + input.Bind(wx.EVT_CHOICE, self.update_choice_state) + self.dict_of_choices[param.name] = { + "param": param, "widget": input, "last_initialized_choice": 1} elif len(param.values) > 1: - input = wx.ComboBox(self, wx.ID_ANY, choices=sorted(str(value) for value in param.values), style=wx.CB_DROPDOWN) + input = wx.ComboBox(self, wx.ID_ANY, choices=sorted( + str(value) for value in param.values), style=wx.CB_DROPDOWN) input.Bind(wx.EVT_COMBOBOX, self.changed) input.Bind(wx.EVT_TEXT, self.changed) else: @@ -298,27 +363,42 @@ class ParamsTab(ScrolledPanel): self.param_inputs[param.name] = input - self.settings_grid.Add(input, proportion=1, flag=wx.ALIGN_CENTER_VERTICAL | wx.EXPAND | wx.LEFT, border=40) - self.settings_grid.Add(wx.StaticText(self, label=param.unit or ""), proportion=1, flag=wx.ALIGN_CENTER_VERTICAL) + col4 = wx.StaticText(self, label=param.unit or "") + + if param.select_items is not None: + input.Hide() + col4.Hide() + for item in param.select_items: + self.choice_widgets[item].extend([input, col4]) + # else: + self.settings_grid.Add( + input, proportion=1, flag=wx.ALIGN_CENTER_VERTICAL | wx.EXPAND | wx.LEFT, border=40) + self.settings_grid.Add( + col4, proportion=1, flag=wx.ALIGN_CENTER_VERTICAL) self.inputs_to_params = {v: k for k, v in self.param_inputs.items()} box.Add(self.settings_grid, proportion=1, flag=wx.ALL, border=10) self.SetSizer(box) + self.update_choice_widgets() self.Layout() def create_change_indicator(self, param): - indicator = wx.Button(self, style=wx.BORDER_NONE | wx.BU_NOTEXT, size=(28, 28)) - indicator.SetToolTip(_('Click to force this parameter to be saved when you click "Apply and Quit"')) - indicator.Bind(wx.EVT_BUTTON, lambda event: self.enable_change_indicator(param)) + indicator = wx.Button(self, style=wx.BORDER_NONE | + wx.BU_NOTEXT, size=(28, 28)) + indicator.SetToolTip( + _('Click to force this parameter to be saved when you click "Apply and Quit"')) + indicator.Bind( + wx.EVT_BUTTON, lambda event: self.enable_change_indicator(param)) self.param_change_indicators[param] = indicator return indicator def enable_change_indicator(self, param): self.param_change_indicators[param].SetBitmapLabel(self.pencil_icon) - self.param_change_indicators[param].SetToolTip(_('This parameter will be saved when you click "Apply and Quit"')) + self.param_change_indicators[param].SetToolTip( + _('This parameter will be saved when you click "Apply and Quit"')) self.changed_inputs.add(self.param_inputs[param]) @@ -344,7 +424,8 @@ class SettingsFrame(wx.Frame): _("Embroidery Params") ) - icon = wx.Icon(os.path.join(get_resource_dir("icons"), "inkstitch256x256.png")) + icon = wx.Icon(os.path.join( + get_resource_dir("icons"), "inkstitch256x256.png")) self.SetIcon(icon) self.notebook = wx.Notebook(self, wx.ID_ANY) @@ -362,7 +443,8 @@ class SettingsFrame(wx.Frame): self.cancel_button.Bind(wx.EVT_BUTTON, self.cancel) self.Bind(wx.EVT_CLOSE, self.cancel) - self.use_last_button = wx.Button(self, wx.ID_ANY, _("Use Last Settings")) + self.use_last_button = wx.Button( + self, wx.ID_ANY, _("Use Last Settings")) self.use_last_button.Bind(wx.EVT_BUTTON, self.use_last) self.apply_button = wx.Button(self, wx.ID_ANY, _("Apply and Quit")) @@ -481,7 +563,8 @@ class SettingsFrame(wx.Frame): for tab in self.tabs: self.notebook.AddPage(tab, tab.name) sizer_1.Add(self.warning_panel, 0, flag=wx.EXPAND | wx.ALL, border=10) - sizer_1.Add(self.notebook, 1, wx.EXPAND | wx.LEFT | wx.TOP | wx.RIGHT, 10) + sizer_1.Add(self.notebook, 1, wx.EXPAND | + wx.LEFT | wx.TOP | wx.RIGHT, 10) sizer_1.Add(self.presets_panel, 0, flag=wx.EXPAND | wx.ALL, border=10) sizer_3.Add(self.cancel_button, 0, wx.RIGHT, 5) sizer_3.Add(self.use_last_button, 0, wx.RIGHT | wx.BOTTOM, 5) @@ -520,8 +603,7 @@ class Params(InkstitchExtension): classes.append(Clone) else: if element.get_style("fill", 'black') and not element.get_style("fill-opacity", 1) == "0": - classes.append(AutoFill) - classes.append(Fill) + classes.append(FillStitch) if element.get_style("stroke") is not None: classes.append(Stroke) if element.get_style("stroke-dasharray") is None: @@ -548,7 +630,8 @@ class Params(InkstitchExtension): else: getter = 'get_param' - values = [item for item in (getattr(node, getter)(param.name, param.default) for node in nodes) if item is not None] + values = [item for item in (getattr(node, getter)( + param.name, param.default) for node in nodes) if item is not None] return values @@ -614,7 +697,8 @@ class Params(InkstitchExtension): for group, params in self.group_params(params): tab_name = group or cls.element_name - tab = ParamsTab(parent, id=wx.ID_ANY, name=tab_name, params=list(params), nodes=nodes) + tab = ParamsTab(parent, id=wx.ID_ANY, name=tab_name, + params=list(params), nodes=nodes) new_tabs.append(tab) if group == "": @@ -634,14 +718,16 @@ class Params(InkstitchExtension): def effect(self): try: app = wx.App() - frame = SettingsFrame(tabs_factory=self.create_tabs, on_cancel=self.cancel) + frame = SettingsFrame( + tabs_factory=self.create_tabs, on_cancel=self.cancel) # position left, center current_screen = wx.Display.GetFromPoint(wx.GetMousePosition()) display = wx.Display(current_screen) display_size = display.GetClientArea() frame_size = frame.GetSize() - frame.SetPosition((int(display_size[0]), int(display_size[3]/2 - frame_size[1]/2))) + frame.SetPosition((int(display_size[0]), int( + display_size[3]/2 - frame_size[1]/2))) frame.Show() app.MainLoop() diff --git a/lib/extensions/print_pdf.py b/lib/extensions/print_pdf.py index e5cb25d8..63c3c699 100644 --- a/lib/extensions/print_pdf.py +++ b/lib/extensions/print_pdf.py @@ -73,7 +73,10 @@ class PrintPreviewServer(Thread): def __set_resources_path(self): if getattr(sys, 'frozen', False): - self.resources_path = os.path.join(sys._MEIPASS, 'print', 'resources') + if sys.platform == "darwin": + self.resources_path = os.path.join(sys._MEIPASS, "..", 'Resources', 'print', 'resources') + else: + self.resources_path = os.path.join(sys._MEIPASS, 'print', 'resources') else: self.resources_path = os.path.realpath(os.path.join(os.path.dirname(__file__), '..', '..', 'print', 'resources')) @@ -183,7 +186,10 @@ class PrintPreviewServer(Thread): class Print(InkstitchExtension): def build_environment(self): if getattr(sys, 'frozen', False): - print_dir = os.path.join(sys._MEIPASS, "print") + if sys.platform == "darwin": + print_dir = os.path.join(sys._MEIPASS, "..", 'Resources', "print") + else: + print_dir = os.path.join(sys._MEIPASS, "print") else: print_dir = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..", "print")) @@ -281,7 +287,7 @@ class Print(InkstitchExtension): 'num_trims': stitch_plan.num_trims, 'dimensions': stitch_plan.dimensions_mm, 'num_stitches': stitch_plan.num_stitches, - 'estimated_thread': '', # TODO + 'estimated_thread': stitch_plan.estimated_thread }, svg_overview=overview_svg, color_blocks=stitch_plan.color_blocks, @@ -295,7 +301,7 @@ class Print(InkstitchExtension): # objects. It's almost certain they meant to print the whole design. # If they really wanted to print just a few objects, they could set # the rest invisible temporarily. - self.svg.selected.clear() + self.svg.selection.clear() if not self.get_elements(): return diff --git a/lib/extensions/remove_embroidery_settings.py b/lib/extensions/remove_embroidery_settings.py index 223e4811..d8e6cb0e 100644 --- a/lib/extensions/remove_embroidery_settings.py +++ b/lib/extensions/remove_embroidery_settings.py @@ -3,11 +3,10 @@ # Copyright (c) 2010 Authors # Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. -from inkex import NSS, Boolean +from inkex import NSS, Boolean, ShapeElement from ..commands import find_commands from ..svg.svg import find_elements -from ..svg.tags import EMBROIDERABLE_TAGS, SVG_GROUP_TAG from .base import InkstitchExtension @@ -36,7 +35,7 @@ class RemoveEmbroiderySettings(InkstitchExtension): self.remove_element(print_setting) def remove_params(self): - if not self.svg.selected: + if not self.svg.selection: xpath = ".//svg:path|.//svg:circle|.//svg:rect|.//svg:ellipse" elements = find_elements(self.svg, xpath) self.remove_inkstitch_attributes(elements) @@ -45,23 +44,22 @@ class RemoveEmbroiderySettings(InkstitchExtension): self.remove_inkstitch_attributes(elements) def remove_commands(self): - if not self.svg.selected: - # we are not able to grab commands by a specific id - # so let's move through every object instead and see if it has a command - xpath = ".//svg:path|.//svg:circle|.//svg:rect|.//svg:ellipse" - elements = find_elements(self.svg, xpath) + 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: + group.getparent().remove(group) else: elements = self.get_selected_elements() - - if elements: for element in elements: for command in find_commands(element): group = command.connector.getparent() group.getparent().remove(group) - if not self.svg.selected: - # remove standalone commands - standalone_commands = ".//svg:use[starts-with(@xlink:href, '#inkstitch_')]" + 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 @@ -69,14 +67,7 @@ class RemoveEmbroiderySettings(InkstitchExtension): self.remove_elements(symbols) def get_selected_elements(self): - elements = [] - for node in self.svg.selected.values(): - if node.tag == SVG_GROUP_TAG: - for child in node.iterdescendants(EMBROIDERABLE_TAGS): - elements.append(child) - else: - elements.append(node) - return elements + return self.svg.selection.get(ShapeElement) def remove_elements(self, xpath): elements = find_elements(self.svg, xpath) diff --git a/lib/extensions/reorder.py b/lib/extensions/reorder.py index 933c1d70..956c0615 100644 --- a/lib/extensions/reorder.py +++ b/lib/extensions/reorder.py @@ -3,18 +3,26 @@ # Copyright (c) 2010 Authors # Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. +from inkex import errormsg + +from ..i18n import _ from .base import InkstitchExtension class Reorder(InkstitchExtension): - # Remove selected objects from the document and readd them in the order they + # Remove selected objects from the document and re-add them in the order they # were selected. def effect(self): - objects = self.get_selected_in_order() + objects = self.svg.selection + + if not objects: + errormsg(_("Please select at least two elements to reorder.")) + return - for obj in objects[1:]: - obj.getparent().remove(obj) + for obj in objects: + if not obj == objects.first(): + obj.getparent().remove(obj) insert_parent = objects[0].getparent() insert_pos = insert_parent.index(objects[0]) diff --git a/lib/extensions/selection_to_guide_line.py b/lib/extensions/selection_to_guide_line.py new file mode 100644 index 00000000..a0d32601 --- /dev/null +++ b/lib/extensions/selection_to_guide_line.py @@ -0,0 +1,26 @@ +# Authors: see git history +# +# Copyright (c) 2021 Authors +# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. + +import inkex + +from ..i18n import _ +from ..marker import set_marker +from ..svg.tags import EMBROIDERABLE_TAGS +from .base import InkstitchExtension + + +class SelectionToGuideLine(InkstitchExtension): + + def effect(self): + if not self.get_elements(): + return + + if not self.svg.selected: + inkex.errormsg(_("Please select at least one object to be marked as a guide line.")) + return + + for pattern in self.get_nodes(): + if pattern.tag in EMBROIDERABLE_TAGS: + set_marker(pattern, 'start', 'guide-line') diff --git a/lib/extensions/selection_to_pattern.py b/lib/extensions/selection_to_pattern.py index 41f89a83..8b41ff86 100644 --- a/lib/extensions/selection_to_pattern.py +++ b/lib/extensions/selection_to_pattern.py @@ -4,10 +4,10 @@ # Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. import inkex -from lxml import etree from ..i18n import _ -from ..svg.tags import EMBROIDERABLE_TAGS, SVG_DEFS_TAG +from ..marker import set_marker +from ..svg.tags import EMBROIDERABLE_TAGS from .base import InkstitchExtension @@ -17,47 +17,10 @@ class SelectionToPattern(InkstitchExtension): if not self.get_elements(): return - if not self.svg.selected: + if not self.svg.selection: inkex.errormsg(_("Please select at least one object to be marked as a pattern.")) return for pattern in self.get_nodes(): if pattern.tag in EMBROIDERABLE_TAGS: - self.set_marker(pattern) - - def set_marker(self, node): - xpath = ".//marker[@id='inkstitch-pattern-marker']" - pattern_marker = self.document.xpath(xpath) - - if not pattern_marker: - # get or create def element - defs = self.document.find(SVG_DEFS_TAG) - if defs is None: - defs = etree.SubElement(self.document, SVG_DEFS_TAG) - - # insert marker - marker = """<marker - refX="10" - refY="5" - orient="auto" - id="inkstitch-pattern-marker"> - <g - id="inkstitch-pattern-group"> - <path - style="fill:#fafafa;stroke:#ff5500;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:1, 1;stroke-dashoffset:0;stroke-opacity:1;fill-opacity:0.8;" - d="M 10.12911,5.2916678 A 4.8374424,4.8374426 0 0 1 5.2916656,10.12911 4.8374424,4.8374426 0 0 1 0.45422399,5.2916678 4.8374424,4.8374426 0 0 1 5.2916656,0.45422399 4.8374424,4.8374426 0 0 1 10.12911,5.2916678 Z" - id="inkstitch-pattern-marker-circle" /> - <path - style="fill:none;stroke:#000000;stroke-width:0.4;stroke-linecap:round;stroke-miterlimit:4;" - id="inkstitch-pattern-marker-spiral" - d="M 4.9673651,5.7245662 C 4.7549848,5.7646159 4.6247356,5.522384 4.6430021,5.3419847 4.6765851,5.0103151 5.036231,4.835347 5.3381858,4.8987426 5.7863901,4.9928495 6.0126802,5.4853625 5.9002872,5.9065088 5.7495249,6.4714237 5.1195537,6.7504036 4.5799191,6.5874894 3.898118,6.3816539 3.5659013,5.6122905 3.7800789,4.9545192 4.0402258,4.1556558 4.9498996,3.7699484 5.7256318,4.035839 6.6416744,4.3498087 7.0810483,5.4003986 6.7631909,6.2939744 6.395633,7.3272552 5.2038143,7.8204128 4.1924535,7.4503931 3.0418762,7.0294421 2.4948761,5.6961604 2.9171752,4.567073 3.3914021,3.2991406 4.8663228,2.6982592 6.1130974,3.1729158 7.4983851,3.7003207 8.1531869,5.3169977 7.6260947,6.6814205 7.0456093,8.1841025 5.2870784,8.8928844 3.8050073,8.3132966 2.1849115,7.6797506 1.4221671,5.7793073 2.0542715,4.1796074 2.7408201,2.4420977 4.7832541,1.6253548 6.5005435,2.310012 8.3554869,3.0495434 9.2262638,5.2339874 8.4890181,7.0688861 8.4256397,7.2266036 8.3515789,7.379984 8.2675333,7.5277183" /> - </g> - </marker>""" # noqa: E501 - defs.append(etree.fromstring(marker)) - - # attach marker to node - style = node.get('style') or '' - style = style.split(";") - style = [i for i in style if not i.startswith('marker-start')] - style.append('marker-start:url(#inkstitch-pattern-marker)') - node.set('style', ";".join(style)) + set_marker(pattern, 'start', 'pattern') diff --git a/lib/extensions/stitch_plan_preview.py b/lib/extensions/stitch_plan_preview.py index c50fa738..e5e570fb 100644 --- a/lib/extensions/stitch_plan_preview.py +++ b/lib/extensions/stitch_plan_preview.py @@ -3,12 +3,23 @@ # Copyright (c) 2010 Authors # Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. +from inkex import Boolean, Style +from lxml import etree + from ..stitch_plan import stitch_groups_to_stitch_plan from ..svg import render_stitch_plan +from ..svg.tags import (INKSCAPE_GROUPMODE, SVG_DEFS_TAG, SVG_GROUP_TAG, + SVG_PATH_TAG) from .base import InkstitchExtension class StitchPlanPreview(InkstitchExtension): + def __init__(self, *args, **kwargs): + InkstitchExtension.__init__(self, *args, **kwargs) + self.arg_parser.add_argument("-s", "--move-to-side", type=Boolean, default=True, dest="move_to_side") + self.arg_parser.add_argument("-v", "--layer-visibility", type=int, default=0, dest="layer_visibility") + self.arg_parser.add_argument("-n", "--needle-points", type=Boolean, default=False, dest="needle_points") + def effect(self): # delete old stitch plan svg = self.document.getroot() @@ -27,6 +38,53 @@ class StitchPlanPreview(InkstitchExtension): stitch_plan = stitch_groups_to_stitch_plan(patches, collapse_len=collapse_len) render_stitch_plan(svg, stitch_plan, realistic) - # translate stitch plan to the right side of the canvas + # apply options layer = svg.find(".//*[@id='__inkstitch_stitch_plan__']") - layer.set('transform', 'translate(%s)' % svg.get('viewBox', '0 0 800 0').split(' ')[2]) + + # update layer visibility 0 = unchanged, 1 = hidden, 2 = lower opacity + if self.options.layer_visibility == 1: + self.hide_all_layers() + layer.set('style', None) + elif self.options.layer_visibility == 2: + for g in self.document.getroot().findall(SVG_GROUP_TAG): + style = g.specified_style() + # check groupmode and exclude stitch_plan layer + # exclude objects which are not displayed at all or already have opacity < 0.4 + if (g.get(INKSCAPE_GROUPMODE) == "layer" and not g == layer and + float(style.get('opacity', 1)) > 0.4 and not style.get('display', 'inline') == 'none'): + style += Style('opacity:0.4') + g.set("style", style) + + # translate stitch plan to the right side of the canvas + if self.options.move_to_side: + layer.set('transform', 'translate(%s)' % svg.get('viewBox', '0 0 800 0').split(' ')[2]) + else: + layer.set('transform', None) + + # display needle points + if self.options.needle_points: + markers = 'marker-mid:url(#inkstitch-needle-point);marker-start:url(#inkstitch-needle-point);marker-end:url(#inkstitch-needle-point)' + for element in layer.iterdescendants(SVG_PATH_TAG): + style = ';'.join([element.get('style'), markers]) + element.set('style', style) + self.ensure_marker() + + def ensure_marker(self): + xpath = ".//svg:marker[@id='inkstitch-needle-point']" + point_marker = self.document.getroot().xpath(xpath) + + if not point_marker: + # get or create def element + defs = self.document.find(SVG_DEFS_TAG) + if defs is None: + defs = etree.SubElement(self.document, SVG_DEFS_TAG) + + # insert marker + marker = """<marker + orient="auto" + id="inkstitch-needle-point"> + <circle + cx="0" cy="0" r="1.5" + style="fill:context-stroke;opacity:0.8;" /> + </marker>""" + defs.append(etree.fromstring(marker)) diff --git a/lib/extensions/troubleshoot.py b/lib/extensions/troubleshoot.py index bf7faf76..f7d979e7 100644 --- a/lib/extensions/troubleshoot.py +++ b/lib/extensions/troubleshoot.py @@ -128,9 +128,13 @@ class Troubleshoot(InkstitchExtension): self.warning_group = warning_group self.type_warning_group = type_warning_group - def add_descriptions(self, problem_types): + def add_descriptions(self, problem_types): # noqa: C901 svg = self.document.getroot() - text_x = str(float(svg.get('viewBox', '0 0 800 0').split(' ')[2]) + 5.0) + + # We could use svg.viewport_width, but then we would need to do unit conversions, + # so let's stay with parsing the viewbox by ourselves + # viewbox values are either separated through white space or commas + text_x = str(float(svg.get('viewBox', '0 0 800 0').replace(",", " ").split()[2]) + 5.0) text_container = inkex.TextElement(attrib={ "x": text_x, @@ -170,6 +174,8 @@ class Troubleshoot(InkstitchExtension): text.append([problem.name, "font-weight: bold; fill: %s;" % text_color]) text.append([problem.description, "font-size: 3px;"]) text.append(["", ""]) + if problem.steps_to_solve: + text.append([_("Possible solutions"), "font-weight: bold; text-decoration: underline; font-size: 4px;"]) for step in problem.steps_to_solve: text.append([step, "font-size: 4px;"]) text.append(["", ""]) |
