From e52886a64a4e76c3fdc49df95c85655da3c4f7f4 Mon Sep 17 00:00:00 2001 From: Kaalleen <36401965+kaalleen@users.noreply.github.com> Date: Sun, 30 Jun 2024 22:49:18 +0200 Subject: Various fixes (#3028) * several thread palette extension fixes * fix svg tartan when original shape is invalid * tartan stroke spaces * style * fix tartan color substituion at pattern start * ripple: do not render too small paths * use less space for params warning headline * fix clone shape path * zip export template fix (typo) * add realistic stitch plan output warning (help tab) --- lib/elements/clone.py | 2 +- lib/extensions/apply_palette.py | 12 ++- lib/extensions/generate_palette.py | 5 +- lib/extensions/palette_split_text.py | 12 ++- lib/extensions/palette_to_text.py | 12 ++- lib/extensions/stroke_to_lpe_satin.py | 2 +- lib/extensions/tartan.py | 13 ++- lib/gui/warnings.py | 2 +- lib/stitches/ripple_stitch.py | 3 + lib/tartan/svg.py | 28 +++++-- lib/tartan/utils.py | 145 ++++++++++++++++++++++++++-------- templates/stitch_plan_preview.xml | 6 ++ templates/zip.xml | 6 +- 13 files changed, 185 insertions(+), 63 deletions(-) diff --git a/lib/elements/clone.py b/lib/elements/clone.py index 55f6465b..9a89b6ff 100644 --- a/lib/elements/clone.py +++ b/lib/elements/clone.py @@ -195,7 +195,7 @@ class Clone(EmbroideryElement): transform = Transform(self.node.composed_transform()) path = path.transform(transform) path = path.to_superpath() - return MultiLineString(path) + return MultiLineString(path[0]) def center(self, source_node): transform = get_node_transform(self.node.getparent()) diff --git a/lib/extensions/apply_palette.py b/lib/extensions/apply_palette.py index ce6c8f5c..cd8c4c94 100644 --- a/lib/extensions/apply_palette.py +++ b/lib/extensions/apply_palette.py @@ -3,7 +3,7 @@ # Copyright (c) 2024 Authors # Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. -from ..elements import FillStitch +from ..elements import Clone, FillStitch from ..threads import ThreadCatalog, ThreadColor from .base import InkstitchExtension @@ -29,6 +29,16 @@ class ApplyPalette(InkstitchExtension): # Iterate through the color blocks to apply colors for element in self.elements: + if isinstance(element, Clone): + # clones use the color of their source element + continue + elif hasattr(element, 'gradient') and element.gradient is not None: + # apply colors to each gradient stop + for i, gradient_style in enumerate(element.gradient.stop_styles): + color = gradient_style['stop-color'] + gradient_style['stop-color'] = palette.nearest_color(ThreadColor(color)).to_hex_str() + continue + nearest_color = palette.nearest_color(ThreadColor(element.color)) if isinstance(element, FillStitch): element.node.style['fill'] = nearest_color.to_hex_str() diff --git a/lib/extensions/generate_palette.py b/lib/extensions/generate_palette.py index f5d7661c..b87bc179 100644 --- a/lib/extensions/generate_palette.py +++ b/lib/extensions/generate_palette.py @@ -62,7 +62,10 @@ class GeneratePalette(InkstitchExtension): def _get_color_from_elements(self, elements): colors = [] for element in elements: - if 'fill' not in element.style.keys() or not isinstance(element, inkex.TextElement): # type(element) != inkex.TextElement: + if element.TAG == 'g': + colors.extend(self._get_color_from_elements(element.getchildren())) + continue + if 'fill' not in element.style.keys() or not isinstance(element, inkex.TextElement): continue color = inkex.Color(element.style['fill']).to_rgb() diff --git a/lib/extensions/palette_split_text.py b/lib/extensions/palette_split_text.py index 19b70782..67549f4f 100644 --- a/lib/extensions/palette_split_text.py +++ b/lib/extensions/palette_split_text.py @@ -3,10 +3,13 @@ # Copyright (c) 2022 Authors # Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. +from tempfile import TemporaryDirectory + import inkex from ..i18n import _ from .base import InkstitchExtension +from .utils.inkex_command import inkscape class PaletteSplitText(InkstitchExtension): @@ -30,7 +33,14 @@ class PaletteSplitText(InkstitchExtension): transform = text.transform text.pop('transform') - bbox = text.get_inkscape_bbox() + # the inkex command `bbox = text.get_inkscape_bbox()` is causing problems for our pyinstaller bundled + # releases, this code block is taken from inkex/elements/_text + with TemporaryDirectory(prefix="inkscape-command") as tmpdir: + svg_file = inkex.command.write_svg(text.root, tmpdir, "input.svg") + bbox = inkscape(svg_file, "-X", "-Y", "-W", "-H", query_id=text.get_id()) + bbox = list(map(text.root.viewport_to_unit, bbox.splitlines())) + bbox = inkex.BoundingBox.new_xywh(*bbox[1:]) + x = bbox.left y = bbox.bottom height = bbox.height / (len(lines)) diff --git a/lib/extensions/palette_to_text.py b/lib/extensions/palette_to_text.py index db0c50cf..729c92fc 100644 --- a/lib/extensions/palette_to_text.py +++ b/lib/extensions/palette_to_text.py @@ -8,6 +8,7 @@ import os import inkex from ..i18n import _ +from ..svg.tags import INKSCAPE_GROUPMODE, INKSCAPE_LABEL from ..threads.palette import ThreadPalette from .base import InkstitchExtension @@ -31,11 +32,15 @@ class PaletteToText(InkstitchExtension): inkex.errormsg(_("Cannot read palette: invalid GIMP palette header")) return - current_layer = self.svg.get_current_layer() + layer = inkex.Group(attrib={ + 'id': '__inkstitch_palette__', + INKSCAPE_LABEL: _('Thread Palette') + f': {thread_palette.name}', + INKSCAPE_GROUPMODE: 'layer', + }) + self.svg.append(layer) x = 0 y = 0 - pos = 0 for color in thread_palette: line = "%s %s" % (color.name, color.number) element = inkex.TextElement() @@ -43,10 +48,9 @@ class PaletteToText(InkstitchExtension): 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) + layer.append(element) y = float(y) + 5 - pos += 1 if __name__ == '__main__': diff --git a/lib/extensions/stroke_to_lpe_satin.py b/lib/extensions/stroke_to_lpe_satin.py index 3f6e87a2..a71bbb71 100644 --- a/lib/extensions/stroke_to_lpe_satin.py +++ b/lib/extensions/stroke_to_lpe_satin.py @@ -171,7 +171,7 @@ class SatinPattern: def get_path(self, add_rungs, min_width, max_width, length, to_unit): # scale the pattern path to fit the unit of the current svg - scale_factor = scale_factor = 1 / inkex.units.convert_unit('1mm', f'{to_unit}') + scale_factor = 1 / inkex.units.convert_unit('1mm', f'{to_unit}') pattern_path = inkex.Path(self.path).transform(inkex.Transform(f'scale({scale_factor})'), True) # create a path element diff --git a/lib/extensions/tartan.py b/lib/extensions/tartan.py index 8c3c8c5f..3acb659c 100644 --- a/lib/extensions/tartan.py +++ b/lib/extensions/tartan.py @@ -28,11 +28,8 @@ class Tartan(InkstitchExtension): def get_tartan_elements(self): if self.svg.selection: - self._get_elements() - - def _get_elements(self): - for node in self.svg.selection: - self.get_selection(node) + for node in self.svg.selection: + self.get_selection(node) def get_selection(self, node): if node.TAG == 'g' and not node.get_id().startswith('inkstitch-tartan'): @@ -40,13 +37,13 @@ class Tartan(InkstitchExtension): self.get_selection(child_node) else: node = self.get_outline(node) - if node.tag in EMBROIDERABLE_TAGS and node.style('fill'): + if node.tag in EMBROIDERABLE_TAGS and node.style('fill') is not None: self.elements.add(node) def get_outline(self, node): # existing tartans are marked through their outline element # we have either selected the element itself or some other element within a tartan group - if node.get(INKSTITCH_TARTAN, None): + if node.get(INKSTITCH_TARTAN, None) is not None: return node if node.get_id().startswith('inkstitch-tartan'): for element in node.iterchildren(EMBROIDERABLE_TAGS): @@ -55,7 +52,7 @@ class Tartan(InkstitchExtension): for group in node.iterancestors(SVG_GROUP_TAG): if group.get_id().startswith('inkstitch-tartan'): for element in group.iterchildren(EMBROIDERABLE_TAGS): - if element.get(INKSTITCH_TARTAN, None): + if element.get(INKSTITCH_TARTAN, None) is not None: return element # if we don't find an existing tartan, return node return node diff --git a/lib/gui/warnings.py b/lib/gui/warnings.py index c8359fc9..18d91532 100644 --- a/lib/gui/warnings.py +++ b/lib/gui/warnings.py @@ -20,7 +20,7 @@ class WarningPanel(wx.Panel): self.warning = wx.StaticText(self) self.warning.SetLabel(_("An error occurred while rendering the stitch plan:")) self.warning.SetForegroundColour(wx.Colour(255, 25, 25)) - self.main_sizer.Add(self.warning, 1, wx.LEFT | wx.BOTTOM | wx.EXPAND, 10) + self.main_sizer.Add(self.warning, 0, wx.LEFT | wx.BOTTOM | wx.EXPAND, 10) tc_style = wx.TE_MULTILINE | wx.TE_READONLY | wx.VSCROLL | wx.TE_RICH2 self.warning_text = wx.TextCtrl(self, size=(300, 300), style=tc_style) diff --git a/lib/stitches/ripple_stitch.py b/lib/stitches/ripple_stitch.py index a354166c..e150945e 100644 --- a/lib/stitches/ripple_stitch.py +++ b/lib/stitches/ripple_stitch.py @@ -24,6 +24,9 @@ def ripple_stitch(stroke): If more sublines are present interpolation will take place between the first two. ''' + if stroke.as_multi_line_string().length < 0.1: + return [] + is_linear, helper_lines = _get_helper_lines(stroke) num_lines = len(helper_lines[0]) diff --git a/lib/tartan/svg.py b/lib/tartan/svg.py index 739315cf..93d33253 100644 --- a/lib/tartan/svg.py +++ b/lib/tartan/svg.py @@ -24,7 +24,7 @@ from ..stitches.auto_fill import (PathEdge, build_fill_stitch_graph, from ..svg import PIXELS_PER_MM, get_correction_transform from ..utils import DotDict, ensure_multi_line_string from .palette import Palette -from .utils import sort_fills_and_strokes, stripes_to_shapes +from .utils import sort_fills_and_strokes, stripes_to_shapes, get_palette_width class TartanSvgGroup: @@ -51,6 +51,8 @@ class TartanSvgGroup: self.symmetry = self.palette.symmetry self.stripes = self.palette.palette_stripes self.warp, self.weft = self.stripes + self.warp_width = get_palette_width(settings) + self.weft_width = get_palette_width(settings) if self.palette.get_palette_width(self.scale, self.min_stripe_width) == 0: self.warp = [] if self.palette.get_palette_width(self.scale, self.min_stripe_width, 1) == 0: @@ -80,8 +82,16 @@ class TartanSvgGroup: index = parent_group.index(outline) parent_group.insert(index, group) - outline_shape = FillStitch(outline).shape transform = get_correction_transform(outline) + outline_shapes = FillStitch(outline).shape + for outline_shape in outline_shapes.geoms: + self._generate_tartan_group_elements(group, outline_shape, transform) + + # set outline invisible + outline.style['display'] = 'none' + group.append(outline) + + def _generate_tartan_group_elements(self, group, outline_shape, transform): dimensions, rotation_center = self._get_dimensions(outline_shape) warp = stripes_to_shapes( @@ -129,11 +139,6 @@ class TartanSvgGroup: for element in stroke_elements: group.append(element) - # set outline invisible - outline.style['display'] = 'none' - group.append(outline) - return group - def _get_command_position(self, fill: FillStitch, point: Tuple[float, float]) -> Point: """ Shift command position out of the element shape @@ -492,6 +497,8 @@ class TartanSvgGroup: """ Calculates the dimensions for the tartan pattern. Make sure it is big enough for pattern rotations. + We also need additional space to ensure fill stripes go to their full extend, this might be problematic if + start or end stripes use render mode 2 (stroke spacing). :param outline: the shape to be filled with a tartan pattern :returns: [0] a list with boundaries and [1] the center point (for rotations) @@ -509,6 +516,13 @@ class TartanSvgGroup: miny = center.y - min_radius maxx = center.x + min_radius maxy = center.y + min_radius + + extra_space = max(self.warp_width * PIXELS_PER_MM, self.weft_width * PIXELS_PER_MM) + minx -= extra_space + maxx += extra_space + miny -= extra_space + maxy += extra_space + return (float(minx), float(miny), float(maxx), float(maxy)), center def _polygon_to_path( diff --git a/lib/tartan/utils.py b/lib/tartan/utils.py index 4f64fc6f..7949505d 100644 --- a/lib/tartan/utils.py +++ b/lib/tartan/utils.py @@ -46,63 +46,138 @@ def stripes_to_shapes( :returns: a dictionary with shapes grouped by color """ + full_sett = _stripes_to_sett(stripes, symmetry, scale, min_stripe_width) + minx, miny, maxx, maxy = dimensions shapes: defaultdict = defaultdict(list) - original_stripes = stripes - if len(original_stripes) == 0: + if len(full_sett) == 0: return shapes left = minx top = miny - add_to_stroke = 0 - add_to_fill = 0 i = -1 while True: i += 1 - stripes = original_stripes - - segments = stripes - if symmetry and i % 2 != 0 and len(stripes) > 1: - segments = list(reversed(stripes[1:-1])) - for stripe in segments: - width = stripe['width'] * PIXELS_PER_MM * (scale / 100) + for stripe in full_sett: + width = stripe['width'] right = left + width bottom = top + width - if ((top > maxy and weft) or (left > maxx and not weft) or - (add_to_stroke > maxy and weft) or (add_to_stroke > maxx and not weft)): + if (top > maxy and weft) or (left > maxx and not weft): return _merge_polygons(shapes, outline, intersect_outline) - if stripe['render'] == 0: - left = right + add_to_stroke - top = bottom + add_to_stroke - add_to_stroke = 0 - continue - elif stripe['render'] == 2: - add_to_stroke += width + if stripe['color'] is None or not stripe['render']: + left = right + top = bottom continue shape_dimensions = [top, bottom, left, right, minx, miny, maxx, maxy] - if width <= min_stripe_width * PIXELS_PER_MM: - add_to_fill = add_to_stroke - shape_dimensions[0] += add_to_stroke - shape_dimensions[2] += add_to_stroke + if stripe['is_stroke']: linestrings = _get_linestrings(outline, shape_dimensions, rotation, rotation_center, weft) shapes[stripe['color']].extend(linestrings) - add_to_stroke += width - continue - add_to_stroke = 0 + else: + polygon = _get_polygon(shape_dimensions, rotation, rotation_center, weft) + shapes[stripe['color']].append(polygon) + left = right + top = bottom + return shapes + + +def _stripes_to_sett( + stripes: List[dict], + symmetry: bool, + scale: int, + min_stripe_width: float, +) -> List[dict]: + """ + Builds a full sett for easier conversion into elements + + :param stripes: a list of dictionaries with stripe information + :param symmetry: reflective sett (True) / repeating sett (False) + :param scale: the scale value (percent) for the pattern + :param min_stripe_width: min stripe width before it is rendered as running stitch + :returns: a list of dictionaries with stripe information (color, width, is_stroke, render) + """ - # add the space of the lines to the following object to avoid bad symmetry - shape_dimensions[1] += add_to_fill - shape_dimensions[3] += add_to_fill + last_fill_color = _get_last_fill_color(stripes, scale, min_stripe_width, symmetry) + first_was_stroke = False + last_was_stroke = False + add_width = 0 + sett = [] + for stripe in stripes: + width = stripe['width'] * PIXELS_PER_MM * (scale / 100) + is_stroke = width <= min_stripe_width * PIXELS_PER_MM + render = stripe['render'] + + if render == 0: + sett.append({'color': None, 'width': width + add_width, 'is_stroke': False, 'render': False}) + last_fill_color = None + add_width = 0 + last_was_stroke = False + continue + + if render == 2: + sett.append({'color': last_fill_color, 'width': width + add_width, 'is_stroke': False, 'render': True}) + add_width = 0 + last_was_stroke = False + continue + + if is_stroke: + if len(sett) == 0: + first_was_stroke = True + width /= 2 + sett.append({'color': last_fill_color, 'width': width + add_width, 'is_stroke': False, 'render': True}) + sett.append({'color': stripe['color'], 'width': 0, 'is_stroke': True, 'render': True}) + add_width = width + last_was_stroke = True + else: + sett.append({'color': stripe['color'], 'width': width + add_width, 'is_stroke': False, 'render': True}) + last_fill_color = stripe['color'] + last_was_stroke = False + + if add_width > 0: + sett.append({'color': last_fill_color, 'width': add_width, 'is_stroke': False, 'render': True}) + + # For symmetric setts we want to mirror the sett and append to receive a full sett + # We do not repeat at pivot points, which means we exclude the first and the last list item from the mirror + if symmetry: + reversed_sett = list(reversed(sett[1:-1])) + if first_was_stroke: + reversed_sett = reversed_sett[:-1] + if last_was_stroke: + reversed_sett = reversed_sett[1:] + sett.extend(reversed_sett) + + return sett + + +def _get_last_fill_color(stripes: List[dict], scale: int, min_stripe_width: float, symmetry: bool,) -> List[dict]: + ''' + Returns the first fill color of a pattern to substitute spaces if the pattern starts with strokes or + stripes with render mode 2 - polygon = _get_polygon(shape_dimensions, rotation, rotation_center, weft) - shapes[stripe['color']].append(polygon) - left = right + add_to_fill - top = bottom + add_to_fill - add_to_fill = 0 + :param stripes: a list of dictionaries with stripe information + :param scale: the scale value (percent) for the pattern + :param min_stripe_width: min stripe width before it is rendered as running stitch + :param symmetry: reflective sett (True) / repeating sett (False) + :returns: a list with fill colors or a list with one None item if there are no fills + ''' + fill_colors = [] + for stripe in stripes: + if stripe['render'] == 0: + fill_colors.append(None) + elif stripe['render'] == 2: + continue + elif stripe['width'] * (scale / 100) > min_stripe_width: + fill_colors.append(stripe['color']) + if len(fill_colors) == 0: + fill_colors = [None] + + if symmetry: + return fill_colors[0] + else: + return fill_colors[-1] def _merge_polygons( diff --git a/templates/stitch_plan_preview.xml b/templates/stitch_plan_preview.xml index 92507b27..f7180915 100644 --- a/templates/stitch_plan_preview.xml +++ b/templates/stitch_plan_preview.xml @@ -37,6 +37,12 @@ + + diff --git a/templates/zip.xml b/templates/zip.xml index 448819bc..c34027eb 100644 --- a/templates/zip.xml +++ b/templates/zip.xml @@ -21,8 +21,8 @@ {%- endif %} {%- endfor %} false - false - false + false + false 0.3 false zip @@ -39,7 +39,7 @@ - + -- cgit v1.2.3