diff --git a/lib/commands.py b/lib/commands.py index a0d81dce1..858a1d5b7 100644 --- a/lib/commands.py +++ b/lib/commands.py @@ -18,9 +18,9 @@ from .svg import (apply_transforms, generate_unique_id, get_correction_transform, get_document, get_node_transform) from .svg.svg import copy_no_children, point_upwards from .svg.tags import (CONNECTION_END, CONNECTION_START, CONNECTOR_TYPE, - INKSCAPE_LABEL, INKSTITCH_ATTRIBS, SVG_SYMBOL_TAG, - SVG_USE_TAG, XLINK_HREF) + INKSCAPE_LABEL, SVG_SYMBOL_TAG, SVG_USE_TAG, XLINK_HREF) from .utils import Point, cache, get_bundled_dir +from .utils.geometry import ensure_multi_polygon COMMANDS = { # L10N command attached to an object @@ -62,6 +62,7 @@ COMMANDS = { OBJECT_COMMANDS = ["starting_point", "ending_point", "target_point", "autoroute_start", "autoroute_end", "stop", "trim", "ignore_object", "satin_cut_point"] +HIDDEN_CONNECTOR_COMMANDS = ["starting_point", "ending_point", "autoroute_start", "autoroute_end"] FREE_MOVEMENT_OBJECT_COMMANDS = ["autoroute_start", "autoroute_end"] LAYER_COMMANDS = ["ignore_layer"] GLOBAL_COMMANDS = ["origin", "stop_position"] @@ -125,24 +126,28 @@ class Command(BaseCommand): raise CommandParseError("connector has no path information") neighbors = [ - (self.get_node_by_url(self.connector.get(CONNECTION_START)), path[0][0][1]), - (self.get_node_by_url(self.connector.get(CONNECTION_END)), path[0][-1][1]) + self.get_node_by_url(self.connector.get(CONNECTION_START)), + self.get_node_by_url(self.connector.get(CONNECTION_END)) ] - self.symbol_is_end = neighbors[0][0].tag != SVG_USE_TAG + self.symbol_is_end = neighbors[0].tag != SVG_USE_TAG if self.symbol_is_end: neighbors.reverse() - if neighbors[0][0].tag != SVG_USE_TAG: + if neighbors[0].tag != SVG_USE_TAG: raise CommandParseError("connector does not point to a use tag") - self.use = neighbors[0][0] + self.use = neighbors[0] - self.symbol = self.get_node_by_url(neighbors[0][0].get(XLINK_HREF)) + self.symbol = self.get_node_by_url(neighbors[0].get(XLINK_HREF)) self.parse_symbol() - self.target: inkex.BaseElement = neighbors[1][0] - self.target_point = neighbors[1][1] + self.target: inkex.BaseElement = neighbors[1] + + pos = [float(self.use.get("x", 0)), float(self.use.get("y", 0))] + transform = get_node_transform(self.use) + pos = inkex.Transform(transform).apply_to_point(pos) + self.target_point = pos def __repr__(self): return "Command('%s', %s)" % (self.command, self.target_point) @@ -331,7 +336,9 @@ def ensure_symbol(svg, command): path = "./*[@id='inkstitch_%s']" % command defs = svg.defs if defs.find(path) is None: - defs.append(deepcopy(symbol_defs().find(path))) + symbol = deepcopy(symbol_defs().find(path)) + symbol.transform = 'scale(0.2)' + defs.append(symbol) def add_group(document, node, command): @@ -373,7 +380,7 @@ def add_connector(document, symbol, command, element): path = inkex.PathElement(attrib={ "id": generate_unique_id(document, "command_connector"), "d": f"M {start_pos[0]},{start_pos[1]} {end_pos[0]},{end_pos[1]}", - "style": "stroke:#000000;stroke-width:1px;stroke-opacity:0.5;fill:none;", + "style": "fill:none;stroke:#000000;stroke-width:1;stroke-opacity:0.5;vector-effect: non-scaling-stroke;-inkscape-stroke: hairline;", CONNECTION_START: f"#{symbol.get('id')}", CONNECTION_END: f"#{element.node.get('id')}", @@ -383,6 +390,8 @@ def add_connector(document, symbol, command, element): if command not in FREE_MOVEMENT_OBJECT_COMMANDS: path.attrib[CONNECTOR_TYPE] = "polyline" + if command in HIDDEN_CONNECTOR_COMMANDS: + path.style['display'] = 'none' symbol.getparent().insert(0, path) @@ -405,60 +414,28 @@ def add_symbol(document, group, command, pos): def get_command_pos(element, index, total): - # Put command symbols 30 pixels out from the shape, spaced evenly around it. + # Put command symbols on the outline of the shape, spaced evenly around it. - # get a line running 30 pixels out from the shape - - if not isinstance(element.shape.buffer(30), shgeo.MultiPolygon): - outline = element.shape.buffer(30).exterior + if element.name == "Stroke": + shape = element.as_multi_line_string() else: - polygons = element.shape.buffer(30).geoms - polygon = polygons[len(polygons)-1] - outline = polygon.exterior - - # find the top center point on the outline and start there - top_center = shgeo.Point(outline.centroid.x, outline.bounds[1]) - start_position = outline.project(top_center, normalized=True) + shape = element.shape + polygon = ensure_multi_polygon(shape.buffer(0.01)).geoms[-1] + outline = polygon.exterior # pick this item's spot around the outline and perturb it a bit to avoid # stacking up commands if they add commands multiple times position = index / float(total) position += random() * 0.05 - position += start_position return outline.interpolate(position, normalized=True) -def remove_legacy_param(element, command): - if command == "trim" or command == "stop": - # If they had the old "TRIM after" or "STOP after" attributes set, - # automatically delete them. The new commands will do the same - # thing. - # - # If we didn't delete these here, then things would get confusing. - # If the user were to delete a "trim" symbol added by this extension - # but the "embroider_trim_after" attribute is still set, then the - # trim would keep happening. - - attribute = "embroider_%s_after" % command - - if attribute in element.node.attrib: - del element.node.attrib[attribute] - - # Attributes have changed to be namespaced. - # Let's check for them as well, they might have automatically changed. - attribute = INKSTITCH_ATTRIBS["%s_after" % command] - - if attribute in element.node.attrib: - del element.node.attrib[attribute] - - def add_commands(element, commands, pos=None): svg = get_document(element.node) for i, command in enumerate(commands): ensure_symbol(svg, command) - remove_legacy_param(element, command) group = add_group(svg, element.node, command) position = pos diff --git a/lib/elements/fill_stitch.py b/lib/elements/fill_stitch.py index 03a8e1547..939d42d05 100644 --- a/lib/elements/fill_stitch.py +++ b/lib/elements/fill_stitch.py @@ -7,7 +7,7 @@ import math import re import numpy as np -from inkex import LinearGradient, Transform +from inkex import LinearGradient from shapely import geometry as shgeo from shapely import set_precision from shapely.errors import GEOSException @@ -22,7 +22,7 @@ from ..stitches import (auto_fill, circular_fill, contour_fill, guided_fill, legacy_fill, linear_gradient_fill, meander_fill, tartan_fill) from ..stitches.linear_gradient_fill import gradient_angle -from ..svg import PIXELS_PER_MM, get_node_transform +from ..svg import PIXELS_PER_MM from ..svg.clip import get_clip_path from ..svg.tags import INKSCAPE_LABEL from ..tartan.utils import get_tartan_settings, get_tartan_stripes @@ -1196,10 +1196,7 @@ class FillStitch(EmbroideryElement): # get target position command = self.get_command('target_point') if command: - pos = [float(command.use.get("x", 0)), float(command.use.get("y", 0))] - transform = get_node_transform(command.use) - pos = Transform(transform).apply_to_point(pos) - target = shgeo.Point(*pos) + target = shgeo.Point(*command.target_point) else: target = shape.centroid stitches = circular_fill( diff --git a/lib/elements/stroke.py b/lib/elements/stroke.py index 14501c87c..96a9307f9 100644 --- a/lib/elements/stroke.py +++ b/lib/elements/stroke.py @@ -6,7 +6,6 @@ from math import ceil import shapely.geometry as shgeo -from inkex import Transform from shapely.errors import GEOSException from ..i18n import _ @@ -15,7 +14,7 @@ from ..stitch_plan import StitchGroup from ..stitches.ripple_stitch import ripple_stitch from ..stitches.running_stitch import (bean_stitch, running_stitch, zigzag_stitch) -from ..svg import get_node_transform, parse_length_with_units +from ..svg import parse_length_with_units from ..svg.clip import get_clip_path from ..threads import ThreadColor from ..utils import Point, cache @@ -542,10 +541,7 @@ class Stroke(EmbroideryElement): def get_ripple_target(self): command = self.get_command('target_point') if command: - pos = [float(command.use.get("x", 0)), float(command.use.get("y", 0))] - transform = get_node_transform(command.use) - pos = Transform(transform).apply_to_point(pos) - return Point(*pos) + return shgeo.Point(*command.target_point) else: return self.shape.centroid diff --git a/lib/extensions/commands_scale_symbols.py b/lib/extensions/commands_scale_symbols.py index 0e5f482a9..fec8daf86 100644 --- a/lib/extensions/commands_scale_symbols.py +++ b/lib/extensions/commands_scale_symbols.py @@ -14,7 +14,8 @@ class CommandsScaleSymbols(InkstitchExtension): self.arg_parser.add_argument("-s", "--size", dest="size", type=int, default=100) def effect(self): - size = self.options.size / 100 + # by default commands are scaled down to 0.2 + size = 0.2 * self.options.size / 100 # scale symbols svg = self.document.getroot() diff --git a/lib/extensions/gradient_blocks.py b/lib/extensions/gradient_blocks.py index b87e5f9e2..8e341cc8e 100644 --- a/lib/extensions/gradient_blocks.py +++ b/lib/extensions/gradient_blocks.py @@ -9,27 +9,23 @@ from inkex import (DirectedLineSegment, LinearGradient, PathElement, Transform, errormsg) from shapely import geometry as shgeo from shapely.affinity import rotate -from shapely.geometry import Point -from shapely.ops import nearest_points, split +from shapely.ops import split -from ..commands import add_commands from ..elements import FillStitch from ..i18n import _ from ..svg import PIXELS_PER_MM, get_correction_transform from ..svg.tags import INKSTITCH_ATTRIBS -from .commands import CommandsExtension from .duplicate_params import get_inkstitch_attributes +from .base import InkstitchExtension -class GradientBlocks(CommandsExtension): +class GradientBlocks(InkstitchExtension): ''' This will break apart fill objects with a gradient fill into solid color blocks with end_row_spacing. ''' - COMMANDS = ['starting_point', 'ending_point'] - def __init__(self, *args, **kwargs): - CommandsExtension.__init__(self, *args, **kwargs) + InkstitchExtension.__init__(self, *args, **kwargs) self.arg_parser.add_argument("--notebook", type=str, default=0.0) self.arg_parser.add_argument("--options", type=str, default=0.0) self.arg_parser.add_argument("--info", type=str, default=0.0) @@ -64,8 +60,6 @@ class GradientBlocks(CommandsExtension): end_row_spacing = element.row_spacing / PIXELS_PER_MM * 2 end_row_spacing = f'{end_row_spacing: .2f}' - previous_color = None - previous_element = None for i, shape in enumerate(fill_shapes): color = attributes[i]['color'] style['fill'] = color @@ -98,29 +92,8 @@ class GradientBlocks(CommandsExtension): block.set('inkstitch:fill_underlay_row_spacing_mm', end_row_spacing) parent.insert(index, block) - if previous_color == color: - self._add_block_commands(block, previous_element) - previous_color = color - previous_element = block parent.remove(element.node) - def _add_block_commands(self, block, previous_element): - current = FillStitch(block) - previous = FillStitch(previous_element) - if previous.shape.is_empty or current.shape.is_empty: - return - nearest = nearest_points(current.shape, previous.shape) - pos_current = self._get_command_postion(current, nearest[0]) - pos_previous = self._get_command_postion(previous, nearest[1]) - add_commands(current, ['ending_point'], pos_current) - add_commands(previous, ['starting_point'], pos_previous) - - def _get_command_postion(self, fill, point): - center = fill.shape.centroid - line = DirectedLineSegment((center.x, center.y), (point.x, point.y)) - pos = line.point_at_length(line.length + 20) - return Point(pos) - def _element_to_path(self, shape): coords = list(shape.exterior.coords) for interior in shape.interiors: diff --git a/lib/extensions/update_svg.py b/lib/extensions/update_svg.py index f620af2d4..d51c1fa1a 100644 --- a/lib/extensions/update_svg.py +++ b/lib/extensions/update_svg.py @@ -3,9 +3,6 @@ # 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 ..update import update_inkstitch_document from .base import InkstitchExtension @@ -14,24 +11,22 @@ class UpdateSvg(InkstitchExtension): def __init__(self, *args, **kwargs): InkstitchExtension.__init__(self, *args, **kwargs) + self.arg_parser.add_argument("--update-from", type=int, default=0, dest="update_from") # inkstitch_svg_version history: # 1 -> v3.0.0, May 2023 # 2 -> v.3.1.0 May 2024 - - # TODO: When there are more legacy versions than only one, this can be transformed into a user input - self.update_from = 0 + # 3 -> v.3.2.0 May2025 def effect(self): if not self.svg.selection: - errormsg(_('Please select at least one element to update. ' - 'This extension is designed to help you update copy and pasted elements from old designs.')) + update_inkstitch_document(self.document, warn_unversioned=False) + else: + # set the file version to the update_from value, so that the updater knows where to start from + # the updater will then reset it to the current version after the update has finished + metadata = self.get_inkstitch_metadata() + metadata['inkstitch_svg_version'] = self.options.update_from - # set the file version to the update_from value, so that the updater knows where to start from - # the updater will then reset it to the current version after the update has finished - metadata = self.get_inkstitch_metadata() - metadata['inkstitch_svg_version'] = self.update_from - - update_inkstitch_document(self.document, self.get_selection()) + update_inkstitch_document(self.document, self.get_selection(), warn_unversioned=False) def get_selection(self): selection = [] diff --git a/lib/gui/tartan/main_panel.py b/lib/gui/tartan/main_panel.py index 9df6a748d..4435f4732 100644 --- a/lib/gui/tartan/main_panel.py +++ b/lib/gui/tartan/main_panel.py @@ -215,10 +215,14 @@ class TartanMainPanel(wx.Panel): self.save_settings() stitch_groups = [] previous_stitch_group = None - for element in self.elements: + next_elements = [None] + if len(self.elements) > 1: + next_elements = self.elements[1:] + next_elements + for element, next_element in zip(self.elements, next_elements): + check_stop_flag() try: # copy the embroidery element to drop the cache - stitch_groups.extend(copy(FillStitch(element)).embroider(previous_stitch_group)) + stitch_groups.extend(copy(FillStitch(element)).embroider(previous_stitch_group, next_element)) if stitch_groups: previous_stitch_group = stitch_groups[-1] except (SystemExit, ExitThread): @@ -241,13 +245,16 @@ class TartanMainPanel(wx.Panel): for element in self.elements: parent = element.getparent() embroidery_elements = nodes_to_elements(parent.iterdescendants()) - for embroidery_element in embroidery_elements: + next_elements = [None] + if len(embroidery_elements) > 1: + next_elements = embroidery_elements[1:] + next_elements + for embroidery_element, next_element in zip(embroidery_elements, next_elements): check_stop_flag() if embroidery_element.node == element: continue try: # copy the embroidery element to drop the cache - stitch_groups.extend(copy(embroidery_element).embroider(previous_stitch_group)) + stitch_groups.extend(copy(embroidery_element).embroider(previous_stitch_group, next_element)) if stitch_groups: previous_stitch_group = stitch_groups[-1] except InkstitchException: diff --git a/lib/marker.py b/lib/marker.py index ac19fe747..9765fbfb3 100644 --- a/lib/marker.py +++ b/lib/marker.py @@ -18,7 +18,9 @@ MARKER = ['anchor-line', 'pattern', 'guide-line'] def ensure_marker(svg, marker): marker_path = ".//*[@id='inkstitch-%s-marker']" % marker if svg.defs.find(marker_path) is None: - svg.defs.append(deepcopy(_marker_svg().defs.find(marker_path))) + marker = deepcopy(_marker_svg().defs.find(marker_path)) + marker.set('markerWidth', str(0.1)) + svg.defs.append(marker) @cache diff --git a/lib/tartan/svg.py b/lib/tartan/svg.py index 15646629f..497d0199a 100644 --- a/lib/tartan/svg.py +++ b/lib/tartan/svg.py @@ -13,10 +13,8 @@ from inkex import BaseElement, Group, Path, PathElement from networkx import MultiGraph, is_empty from shapely import (LineString, MultiLineString, MultiPolygon, Point, Polygon, dwithin, minimum_bounding_radius, reverse) -from shapely.affinity import scale from shapely.ops import linemerge, substring -from ..commands import add_commands from ..elements import FillStitch from ..stitches.auto_fill import (PathEdge, build_fill_stitch_graph, build_travel_graph, find_stitch_path, @@ -127,52 +125,13 @@ class TartanSvgGroup: for color, fill_elements in fills.items(): for element in fill_elements: group.append(element) - if self.stitch_type == "auto_fill": - self._add_command(element) - else: - element.pop('inkstitch:start') - element.pop('inkstitch:end') + element.pop('inkstitch:start') + element.pop('inkstitch:end') for color, stroke_elements in strokes.items(): for element in stroke_elements: group.append(element) - def _get_command_position(self, fill: FillStitch, point: Tuple[float, float]) -> Point: - """ - Shift command position out of the element shape - - :param fill: the fill element to which to attach the command - :param point: position where the command should point to - """ - dimensions, center = self._get_dimensions(fill.shape) - line = LineString([center, point]) - fact = 20 / line.length - line = scale(line, xfact=1+fact, yfact=1+fact, origin=center) - pos = line.coords[-1] - return Point(pos) - - def _add_command(self, element: BaseElement) -> None: - """ - Add a command to given svg element - - :param element: svg element to which to attach the command - """ - if not element.style('fill'): - return - fill = FillStitch(element) - if fill.shape.is_empty: - return - start = element.get('inkstitch:start') - end = element.get('inkstitch:end') - if start: - start = start[1:-1].split(',') - add_commands(fill, ['starting_point'], self._get_command_position(fill, (float(start[0]), float(start[1])))) - element.pop('inkstitch:start') - if end: - end = end[1:-1].split(',') - add_commands(fill, ['ending_point'], self._get_command_position(fill, (float(end[0]), float(end[1])))) - element.pop('inkstitch:end') - def _route_shapes(self, routing_lines: defaultdict, outline_shape: MultiPolygon, shapes: defaultdict, weft: bool = False) -> defaultdict: """ Route polygons and linestrings diff --git a/lib/update.py b/lib/update.py index 33ae6cc7e..4351beb7f 100644 --- a/lib/update.py +++ b/lib/update.py @@ -5,13 +5,15 @@ from inkex import errormsg -from .commands import ensure_symbol -from .elements import EmbroideryElement +from .commands import add_commands, ensure_symbol +from .elements import EmbroideryElement, Stroke from .gui.request_update_svg_version import RequestUpdate from .i18n import _ from .metadata import InkStitchMetadata from .svg import PIXELS_PER_MM -from .svg.tags import EMBROIDERABLE_TAGS, INKSTITCH_ATTRIBS +from .svg.tags import (CONNECTION_END, CONNECTION_START, EMBROIDERABLE_TAGS, + INKSTITCH_ATTRIBS, SVG_USE_TAG) +from .utils import Point as InkstitchPoint INKSTITCH_SVG_VERSION = 3 @@ -48,10 +50,10 @@ def update_inkstitch_document(svg, selection=None, warn_unversioned=True): return # update elements - if selection: - # this comes from the updater extension where we only update selected elements + if selection is not None: + # the updater extension might want to only update selected elements for element in selection: - update_legacy_params(EmbroideryElement(element), file_version, INKSTITCH_SVG_VERSION) + update_legacy_params(document, EmbroideryElement(element), file_version, INKSTITCH_SVG_VERSION) else: # this is the automatic update when a legacy inkstitch svg version was recognized automatic_version_update(document, file_version, INKSTITCH_SVG_VERSION, warn_unversioned) @@ -69,9 +71,7 @@ def automatic_version_update(document, file_version, INKSTITCH_SVG_VERSION, warn # well then, let's update legeacy params for element in document.iterdescendants(): if element.tag in EMBROIDERABLE_TAGS: - update_legacy_params(EmbroideryElement(element), file_version, INKSTITCH_SVG_VERSION) - if file_version < 3: - update_legacy_commands(document) + update_legacy_params(document, EmbroideryElement(element), file_version, INKSTITCH_SVG_VERSION) def _update_inkstitch_svg_version(svg): @@ -80,24 +80,25 @@ def _update_inkstitch_svg_version(svg): metadata['inkstitch_svg_version'] = INKSTITCH_SVG_VERSION -def update_legacy_params(element, file_version, inkstitch_svg_version): +def update_legacy_params(document, element, file_version, inkstitch_svg_version): for version in range(file_version + 1, inkstitch_svg_version + 1): - _update_to(version, element) + _update_to(document, version, element) -def _update_to(version, element): +def _update_to(document, version, element): if version == 1: _update_to_one(element) elif version == 2: _update_to_two(element) elif version == 3: - _update_to_three(element) + _update_to_three(document, element) -def _update_to_three(element): +def _update_to_three(document, element): if element.get_boolean_param('satin_column', False): element.set_param('start_at_nearest_point', False) element.set_param('end_at_nearest_point', False) + update_legacy_commands(document, element) def _update_to_two(element): @@ -193,7 +194,7 @@ Update legacy commands ''' -def update_legacy_commands(document): +def update_legacy_commands(document, element): ''' Changes for svg version 3 ''' @@ -208,6 +209,39 @@ def update_legacy_commands(document): _rename_command(document, symbol, 'inkstitch_run_end', 'autoroute_end') _rename_command(document, symbol, 'inkstitch_ripple_target', 'target_point') + # reposition commands + start = element.get_command('starting_point') + if start: + reposition_legacy_command(start) + end = element.get_command('ending_point') + if end: + reposition_legacy_command(end) + + +def reposition_legacy_command(command): + connector = command.connector + command_group = connector.getparent() + element = command.target + command_name = command.command + + # get new target position + path = command.parse_connector_path() + if len(path) == 0: + pass + neighbors = [ + (command.get_node_by_url(command.connector.get(CONNECTION_START)), path[0][0][1]), + (command.get_node_by_url(command.connector.get(CONNECTION_END)), path[0][-1][1]) + ] + symbol_is_end = neighbors[0][0].tag != SVG_USE_TAG + if symbol_is_end: + neighbors.reverse() + target_point = neighbors[1][1] + + # instead of calculating the transform for the new position, we take the easy route and remove + # the old commands and set new ones + add_commands(Stroke(element), [command_name], InkstitchPoint(*target_point)) + command_group.getparent().remove(command_group) + def _rename_command(document, symbol, old_name, new_name): symbol_id = symbol.get_id() diff --git a/templates/update_svg.xml b/templates/update_svg.xml index 924d97389..3cf6a0bf3 100644 --- a/templates/update_svg.xml +++ b/templates/update_svg.xml @@ -18,7 +18,9 @@ <spacer /> <label>Tip: You can prevent inserting legacy designs into new files by running any Ink/Stitch extension before you copy the design parts (for example open and re-apply parameters on a single element in the document).</label> <spacer /> - <label appearance="header">This extension only updates selected elements.</label> + <label appearance="header">When there is an active selection, only selected elements will be updated.</label> + <spacer /> + <param name="update-from" type="int" min="0" max="2" gui-text="Update from version" appearance="full">0</param> <script> {{ command_tag | safe }} </script>