From 0fcf8bb97ced8df552cd0283b4ea009b6ca42623 Mon Sep 17 00:00:00 2001 From: Andreas Date: Thu, 21 Oct 2021 16:24:40 +0200 Subject: added tangential and guided fill --- lib/elements/__init__.py | 2 +- lib/elements/auto_fill.py | 281 ++++++++++++++++++++++++++++++++++++++++------ lib/elements/clone.py | 10 +- lib/elements/element.py | 4 +- lib/elements/fill.py | 205 --------------------------------- lib/elements/utils.py | 10 +- 6 files changed, 263 insertions(+), 249 deletions(-) delete mode 100644 lib/elements/fill.py (limited to 'lib/elements') diff --git a/lib/elements/__init__.py b/lib/elements/__init__.py index 2e4c31a7..bb5c95ba 100644 --- a/lib/elements/__init__.py +++ b/lib/elements/__init__.py @@ -7,7 +7,7 @@ from .auto_fill import AutoFill from .clone import Clone from .element import EmbroideryElement from .empty_d_object import EmptyDObject -from .fill import Fill +#from .fill import Fill from .image import ImageObject from .polyline import Polyline from .satin_column import SatinColumn diff --git a/lib/elements/auto_fill.py b/lib/elements/auto_fill.py index fbbd86d3..87bdb010 100644 --- a/lib/elements/auto_fill.py +++ b/lib/elements/auto_fill.py @@ -6,18 +6,26 @@ import math import sys import traceback +import re +import logging +import inkex from shapely import geometry as shgeo - -from .element import param -from .fill import Fill -from .validation import ValidationWarning +from shapely.validation import explain_validity +from ..stitches import legacy_fill from ..i18n import _ from ..stitch_plan import StitchGroup from ..stitches import auto_fill -from ..svg.tags import INKSCAPE_LABEL +from ..stitches import StitchPattern from ..utils import cache, version - +from .element import param +from .element import EmbroideryElement +from ..patterns import get_patterns +#from .fill import Fill +from .validation import ValidationWarning +from ..utils import Point as InkstitchPoint +from ..svg import PIXELS_PER_MM +from ..svg.tags import INKSCAPE_LABEL class SmallShapeWarning(ValidationWarning): name = _("Small Fill") @@ -38,13 +46,125 @@ class UnderlayInsetWarning(ValidationWarning): "Ink/Stitch will ignore it and will use the original size instead.") -class AutoFill(Fill): +class AutoFill(EmbroideryElement): element_name = _("AutoFill") @property - @param('auto_fill', _('Automatically routed fill stitching'), type='toggle', default=True) - def auto_fill(self): - return self.get_boolean_param('auto_fill', True) + @param('auto_fill', _('Automatically routed fill stitching'), type='toggle', default=True, sort_index = 1) + def auto_fill2(self): + return self.get_boolean_param('auto_fill', True) + + @property + @param('fill_method', _('Fill method'), type='dropdown', default=0, options=[_("Auto Fill"), _("Tangential"), _("Guided Auto Fill")], sort_index = 2) + def fill_method(self): + return self.get_int_param('fill_method', 0) + + @property + @param('tangential_strategy', _('Tangential strategy'), type='dropdown', default=1, options=[_("Closest point"), _("Inner to Outer")],select_items=[('fill_method',1)], sort_index = 2) + def tangential_strategy(self): + return self.get_int_param('tangential_strategy', 1) + + @property + @param('join_style', _('Join Style'), type='dropdown', default=0, options=[_("Round"), _("Mitered"), _("Beveled")],select_items=[('fill_method',1)], sort_index = 2) + def join_style(self): + return self.get_int_param('join_style', 0) + + @property + @param('interlaced', _('Interlaced'), type='boolean', default=True,select_items=[('fill_method',1),('fill_method',2)], sort_index = 2) + def interlaced(self): + return self.get_boolean_param('interlaced', True) + + @property + @param('angle', + _('Angle of lines of stitches'), + tooltip=_('The angle increases in a counter-clockwise direction. 0 is horizontal. Negative angles are allowed.'), + unit='deg', + type='float', + sort_index = 4, + select_items=[('fill_method',0)], + default=0) + @cache + def angle(self): + return math.radians(self.get_float_param('angle', 0)) + + @property + def color(self): + # SVG spec says the default fill is black + return self.get_style("fill", "#000000") + + @property + @param( + 'skip_last', + _('Skip last stitch in each row'), + tooltip=_('The last stitch in each row is quite close to the first stitch in the next row. ' + 'Skipping it decreases stitch count and density.'), + type='boolean', + sort_index = 4, + select_items=[('fill_method',0), ('fill_method',2)], + default=False) + def skip_last(self): + return self.get_boolean_param("skip_last", False) + + @property + @param( + 'flip', + _('Flip fill (start right-to-left)'), + tooltip=_('The flip option can help you with routing your stitch path. ' + 'When you enable flip, stitching goes from right-to-left instead of left-to-right.'), + type='boolean', + sort_index = 4, + select_items=[('fill_method',0), ('fill_method',2)], + default=False) + def flip(self): + return self.get_boolean_param("flip", False) + + @property + @param('row_spacing_mm', + _('Spacing between rows'), + tooltip=_('Distance between rows of stitches.'), + unit='mm', + sort_index = 4, + type='float', + default=0.25) + def row_spacing(self): + return max(self.get_float_param("row_spacing_mm", 0.25), 0.1 * PIXELS_PER_MM) + + @property + def end_row_spacing(self): + return self.get_float_param("end_row_spacing_mm") + + @property + @param('max_stitch_length_mm', + _('Maximum fill stitch length'), + tooltip=_('The length of each stitch in a row. Shorter stitch may be used at the start or end of a row.'), + unit='mm', + sort_index = 4, + type='float', + default=3.0) + def max_stitch_length(self): + return max(self.get_float_param("max_stitch_length_mm", 3.0), 0.1 * PIXELS_PER_MM) + + @property + @param('staggers', + _('Stagger rows this many times before repeating'), + tooltip=_('Setting this dictates how many rows apart the stitches will be before they fall in the same column position.'), + type='int', + sort_index = 4, + select_items=[('fill_method',0)], + default=4) + def staggers(self): + return max(self.get_int_param("staggers", 4), 1) + + @property + @cache + def paths(self): + paths = self.flatten(self.parse_path()) + # ensure path length + for i, path in enumerate(paths): + if len(path) < 3: + paths[i] = [(path[0][0], path[0][1]), (path[0][0]+1.0, path[0][1]), (path[0][0], path[0][1]+1.0)] + return paths + @property @cache @@ -66,7 +186,9 @@ class AutoFill(Fill): tooltip=_('Length of stitches around the outline of the fill region used when moving from section to section.'), unit='mm', type='float', - default=1.5) + default=1.5, + select_items=[('fill_method',0),('fill_method',2)], + sort_index = 4) def running_stitch_length(self): return max(self.get_float_param("running_stitch_length_mm", 1.5), 0.01) @@ -147,7 +269,9 @@ class AutoFill(Fill): tooltip=_('Expand the shape before fill stitching, to compensate for gaps between shapes.'), unit='mm', type='float', - default=0) + default=0, + sort_index = 5, + select_items=[('fill_method',0),('fill_method',2)]) def expand(self): return self.get_float_param('expand_mm', 0) @@ -158,7 +282,9 @@ class AutoFill(Fill): 'stitches avoid traveling in the direction of the row angle so that they ' 'are not visible. This gives them a jagged appearance.'), type='boolean', - default=True) + default=True, + select_items=[('fill_method',0),('fill_method',2)], + sort_index = 6) def underpath(self): return self.get_boolean_param('underpath', True) @@ -175,6 +301,51 @@ class AutoFill(Fill): def underlay_underpath(self): return self.get_boolean_param('underlay_underpath', True) + @property + @cache + def shape(self): + # shapely's idea of "holes" are to subtract everything in the second set + # from the first. So let's at least make sure the "first" thing is the + # biggest path. + paths = self.paths + paths.sort(key=lambda point_list: shgeo.Polygon(point_list).area, reverse=True) + # Very small holes will cause a shape to be rendered as an outline only + # they are too small to be rendered and only confuse the auto_fill algorithm. + # So let's ignore them + if shgeo.Polygon(paths[0]).area > 5 and shgeo.Polygon(paths[-1]).area < 5: + paths = [path for path in paths if shgeo.Polygon(path).area > 3] + + polygon = shgeo.MultiPolygon([(paths[0], paths[1:])]) + + # There is a great number of "crossing border" errors on fill shapes + # If the polygon fails, we can try to run buffer(0) on the polygon in the + # hope it will fix at least some of them + if not self.shape_is_valid(polygon): + why = explain_validity(polygon) + message = re.match(r".+?(?=\[)", why) + if message.group(0) == "Self-intersection": + buffered = polygon.buffer(0) + # we do not want to break apart into multiple objects (possibly in the future?!) + # best way to distinguish the resulting polygon is to compare the area size of the two + # and make sure users will not experience significantly altered shapes without a warning + if math.isclose(polygon.area, buffered.area): + polygon = shgeo.MultiPolygon([buffered]) + + return polygon + + def shape_is_valid(self, shape): + # Shapely will log to stdout to complain about the shape unless we make + # it shut up. + logger = logging.getLogger('shapely.geos') + level = logger.level + logger.setLevel(logging.CRITICAL) + + valid = shape.is_valid + + logger.setLevel(level) + + return valid + def shrink_or_grow_shape(self, amount, validate=False): if amount: shape = self.shape.buffer(amount) @@ -226,7 +397,8 @@ class AutoFill(Fill): color=self.color, tags=("auto_fill", "auto_fill_underlay"), stitches=auto_fill( - self.underlay_shape, + self.underlay_shape, + None, self.fill_underlay_angle[i], self.fill_underlay_row_spacing, self.fill_underlay_row_spacing, @@ -237,25 +409,70 @@ class AutoFill(Fill): starting_point, underpath=self.underlay_underpath)) stitch_groups.append(underlay) + starting_point = underlay.stitches[-1] + + if self.fill_method == 0: #Auto Fill + stitch_group = StitchGroup( + color=self.color, + tags=("auto_fill", "auto_fill_top"), + stitches=auto_fill( + self.fill_shape, + None, + self.angle, + self.row_spacing, + self.end_row_spacing, + self.max_stitch_length, + self.running_stitch_length, + self.staggers, + self.skip_last, + starting_point, + ending_point, + self.underpath)) + stitch_groups.append(stitch_group) + elif self.fill_method == 1: #Tangential Fill + polygons = list(self.fill_shape) + if not starting_point: + starting_point = (0,0) + for poly in polygons: + connectedLine, connectedLineOrigin = StitchPattern.offset_poly( + poly, + -self.row_spacing, + self.join_style+1, + self.max_stitch_length, + self.interlaced, + self.tangential_strategy, + shgeo.Point(starting_point)) + path = [InkstitchPoint(*p) for p in connectedLine] + stitch_group = StitchGroup( + color=self.color, + tags=("auto_fill", "auto_fill_top"), + stitches=path) + stitch_groups.append(stitch_group) + elif self.fill_method == 2: #Guided Auto Fill + lines = get_patterns(self.node,"#inkstitch-guide-line-marker") + lines = lines['stroke_patterns'] + if not lines or lines[0].is_empty: + inkex.errormsg(_("No line marked as guide line found within the same group as patch")) + else: + stitch_group = StitchGroup( + color=self.color, + tags=("auto_fill", "auto_fill_top"), + stitches=auto_fill( + self.fill_shape, + lines[0].geoms[0], + self.angle, + self.row_spacing, + self.end_row_spacing, + self.max_stitch_length, + self.running_stitch_length, + 0, + self.skip_last, + starting_point, + ending_point, + self.underpath, + self.interlaced)) + stitch_groups.append(stitch_group) - starting_point = underlay.stitches[-1] - - stitch_group = StitchGroup( - color=self.color, - tags=("auto_fill", "auto_fill_top"), - stitches=auto_fill( - self.fill_shape, - self.angle, - self.row_spacing, - self.end_row_spacing, - self.max_stitch_length, - self.running_stitch_length, - self.staggers, - self.skip_last, - starting_point, - ending_point, - self.underpath)) - stitch_groups.append(stitch_group) except Exception: if hasattr(sys, 'gettrace') and sys.gettrace(): # if we're debugging, let the exception bubble up diff --git a/lib/elements/clone.py b/lib/elements/clone.py index f408917d..bcecf3f0 100644 --- a/lib/elements/clone.py +++ b/lib/elements/clone.py @@ -14,7 +14,7 @@ from ..svg.tags import (EMBROIDERABLE_TAGS, INKSTITCH_ATTRIBS, from ..utils import cache from .auto_fill import AutoFill from .element import EmbroideryElement, param -from .fill import Fill +#from .fill import Fill from .polyline import Polyline from .satin_column import SatinColumn from .stroke import Stroke @@ -79,10 +79,10 @@ class Clone(EmbroideryElement): else: elements = [] if element.get_style("fill", "black") and not element.get_style("stroke", 1) == "0": - if element.get_boolean_param("auto_fill", True): - elements.append(AutoFill(node)) - else: - elements.append(Fill(node)) + #if element.get_boolean_param("auto_fill", True): + elements.append(AutoFill(node)) + #else: + # elements.append(Fill(node)) if element.get_style("stroke", self.node) is not None: if not is_command(element.node): elements.append(Stroke(node)) diff --git a/lib/elements/element.py b/lib/elements/element.py index 05bfd353..b8728f60 100644 --- a/lib/elements/element.py +++ b/lib/elements/element.py @@ -20,7 +20,7 @@ from ..utils import Point, cache class Param(object): def __init__(self, name, description, unit=None, values=[], type=None, group=None, inverse=False, - options=[], default=None, tooltip=None, sort_index=0): + options=[], default=None, tooltip=None, sort_index=0, select_items=None): self.name = name self.description = description self.unit = unit @@ -32,6 +32,8 @@ class Param(object): self.default = default self.tooltip = tooltip self.sort_index = sort_index + self.select_items = select_items + #print("IN PARAM: ", self.values) def __repr__(self): return "Param(%s)" % vars(self) diff --git a/lib/elements/fill.py b/lib/elements/fill.py deleted file mode 100644 index 51a6d703..00000000 --- a/lib/elements/fill.py +++ /dev/null @@ -1,205 +0,0 @@ -# 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 logging -import math -import re - -from shapely import geometry as shgeo -from shapely.validation import explain_validity - -from .element import EmbroideryElement, param -from .validation import ValidationError -from ..i18n import _ -from ..stitch_plan import StitchGroup -from ..stitches import legacy_fill -from ..svg import PIXELS_PER_MM -from ..utils import cache - - -class UnconnectedError(ValidationError): - name = _("Unconnected") - description = _("Fill: This object is made up of unconnected shapes. This is not allowed because " - "Ink/Stitch doesn't know what order to stitch them in. Please break this " - "object up into separate shapes.") - steps_to_solve = [ - _('* Extensions > Ink/Stitch > Fill Tools > Break Apart Fill Objects'), - ] - - -class InvalidShapeError(ValidationError): - name = _("Border crosses itself") - description = _("Fill: Shape is not valid. This can happen if the border crosses over itself.") - steps_to_solve = [ - _('* Extensions > Ink/Stitch > Fill Tools > Break Apart Fill Objects') - ] - - -class Fill(EmbroideryElement): - element_name = _("Fill") - - def __init__(self, *args, **kwargs): - super(Fill, self).__init__(*args, **kwargs) - - @property - @param('auto_fill', - _('Manually routed fill stitching'), - tooltip=_('AutoFill is the default method for generating fill stitching.'), - type='toggle', - inverse=True, - default=True) - def auto_fill(self): - return self.get_boolean_param('auto_fill', True) - - @property - @param('angle', - _('Angle of lines of stitches'), - tooltip=_('The angle increases in a counter-clockwise direction. 0 is horizontal. Negative angles are allowed.'), - unit='deg', - type='float', - default=0) - @cache - def angle(self): - return math.radians(self.get_float_param('angle', 0)) - - @property - def color(self): - # SVG spec says the default fill is black - return self.get_style("fill", "#000000") - - @property - @param( - 'skip_last', - _('Skip last stitch in each row'), - tooltip=_('The last stitch in each row is quite close to the first stitch in the next row. ' - 'Skipping it decreases stitch count and density.'), - type='boolean', - default=False) - def skip_last(self): - return self.get_boolean_param("skip_last", False) - - @property - @param( - 'flip', - _('Flip fill (start right-to-left)'), - tooltip=_('The flip option can help you with routing your stitch path. ' - 'When you enable flip, stitching goes from right-to-left instead of left-to-right.'), - type='boolean', - default=False) - def flip(self): - return self.get_boolean_param("flip", False) - - @property - @param('row_spacing_mm', - _('Spacing between rows'), - tooltip=_('Distance between rows of stitches.'), - unit='mm', - type='float', - default=0.25) - def row_spacing(self): - return max(self.get_float_param("row_spacing_mm", 0.25), 0.1 * PIXELS_PER_MM) - - @property - def end_row_spacing(self): - return self.get_float_param("end_row_spacing_mm") - - @property - @param('max_stitch_length_mm', - _('Maximum fill stitch length'), - tooltip=_('The length of each stitch in a row. Shorter stitch may be used at the start or end of a row.'), - unit='mm', - type='float', - default=3.0) - def max_stitch_length(self): - return max(self.get_float_param("max_stitch_length_mm", 3.0), 0.1 * PIXELS_PER_MM) - - @property - @param('staggers', - _('Stagger rows this many times before repeating'), - tooltip=_('Setting this dictates how many rows apart the stitches will be before they fall in the same column position.'), - type='int', - default=4) - def staggers(self): - return max(self.get_int_param("staggers", 4), 1) - - @property - @cache - def paths(self): - paths = self.flatten(self.parse_path()) - # ensure path length - for i, path in enumerate(paths): - if len(path) < 3: - paths[i] = [(path[0][0], path[0][1]), (path[0][0]+1.0, path[0][1]), (path[0][0], path[0][1]+1.0)] - return paths - - @property - @cache - def shape(self): - # shapely's idea of "holes" are to subtract everything in the second set - # from the first. So let's at least make sure the "first" thing is the - # biggest path. - paths = self.paths - paths.sort(key=lambda point_list: shgeo.Polygon(point_list).area, reverse=True) - # Very small holes will cause a shape to be rendered as an outline only - # they are too small to be rendered and only confuse the auto_fill algorithm. - # So let's ignore them - if shgeo.Polygon(paths[0]).area > 5 and shgeo.Polygon(paths[-1]).area < 5: - paths = [path for path in paths if shgeo.Polygon(path).area > 3] - - polygon = shgeo.MultiPolygon([(paths[0], paths[1:])]) - - # There is a great number of "crossing border" errors on fill shapes - # If the polygon fails, we can try to run buffer(0) on the polygon in the - # hope it will fix at least some of them - if not self.shape_is_valid(polygon): - why = explain_validity(polygon) - message = re.match(r".+?(?=\[)", why) - if message.group(0) == "Self-intersection": - buffered = polygon.buffer(0) - # if we receive a multipolygon, only use the first one of it - if type(buffered) == shgeo.MultiPolygon: - buffered = buffered[0] - # we do not want to break apart into multiple objects (possibly in the future?!) - # best way to distinguish the resulting polygon is to compare the area size of the two - # and make sure users will not experience significantly altered shapes without a warning - if type(buffered) == shgeo.Polygon and math.isclose(polygon.area, buffered.area, abs_tol=0.5): - polygon = shgeo.MultiPolygon([buffered]) - - return polygon - - def shape_is_valid(self, shape): - # Shapely will log to stdout to complain about the shape unless we make - # it shut up. - logger = logging.getLogger('shapely.geos') - level = logger.level - logger.setLevel(logging.CRITICAL) - - valid = shape.is_valid - - logger.setLevel(level) - - return valid - - def validation_errors(self): - if not self.shape_is_valid(self.shape): - why = explain_validity(self.shape) - message, x, y = re.findall(r".+?(?=\[)|-?\d+(?:\.\d+)?", why) - - # I Wish this weren't so brittle... - if "Hole lies outside shell" in message: - yield UnconnectedError((x, y)) - else: - yield InvalidShapeError((x, y)) - - def to_stitch_groups(self, last_patch): - stitch_lists = legacy_fill(self.shape, - self.angle, - self.row_spacing, - self.end_row_spacing, - self.max_stitch_length, - self.flip, - self.staggers, - self.skip_last) - return [StitchGroup(stitches=stitch_list, color=self.color) for stitch_list in stitch_lists] diff --git a/lib/elements/utils.py b/lib/elements/utils.py index 99df7002..f858cc81 100644 --- a/lib/elements/utils.py +++ b/lib/elements/utils.py @@ -11,7 +11,7 @@ from .auto_fill import AutoFill from .clone import Clone, is_clone from .element import EmbroideryElement from .empty_d_object import EmptyDObject -from .fill import Fill +#from .fill import Fill from .image import ImageObject from .pattern import PatternObject from .polyline import Polyline @@ -41,10 +41,10 @@ def node_to_elements(node): # noqa: C901 else: elements = [] if element.get_style("fill", "black") and not element.get_style('fill-opacity', 1) == "0": - if element.get_boolean_param("auto_fill", True): - elements.append(AutoFill(node)) - else: - elements.append(Fill(node)) + #if element.get_boolean_param("auto_fill", True): + elements.append(AutoFill(node)) + #else: + # elements.append(Fill(node)) if element.get_style("stroke"): if not is_command(element.node): elements.append(Stroke(node)) -- cgit v1.2.3 From 125db3f83b3b330df757f7cc0faf6489b3cb348d Mon Sep 17 00:00:00 2001 From: Andreas Date: Fri, 29 Oct 2021 16:18:22 +0200 Subject: Applied style guide --- lib/elements/auto_fill.py | 118 +++++++++++++++++++++++++--------------------- lib/elements/clone.py | 5 +- lib/elements/element.py | 22 +++++---- lib/elements/utils.py | 5 +- 4 files changed, 81 insertions(+), 69 deletions(-) (limited to 'lib/elements') diff --git a/lib/elements/auto_fill.py b/lib/elements/auto_fill.py index 87bdb010..81abf7ad 100644 --- a/lib/elements/auto_fill.py +++ b/lib/elements/auto_fill.py @@ -12,7 +12,6 @@ import inkex from shapely import geometry as shgeo from shapely.validation import explain_validity -from ..stitches import legacy_fill from ..i18n import _ from ..stitch_plan import StitchGroup from ..stitches import auto_fill @@ -21,12 +20,12 @@ from ..utils import cache, version from .element import param from .element import EmbroideryElement from ..patterns import get_patterns -#from .fill import Fill from .validation import ValidationWarning from ..utils import Point as InkstitchPoint from ..svg import PIXELS_PER_MM from ..svg.tags import INKSCAPE_LABEL + class SmallShapeWarning(ValidationWarning): name = _("Small Fill") description = _("This fill object is so small that it would probably look better as running stitch or satin column. " @@ -50,38 +49,42 @@ class AutoFill(EmbroideryElement): element_name = _("AutoFill") @property - @param('auto_fill', _('Automatically routed fill stitching'), type='toggle', default=True, sort_index = 1) + @param('auto_fill', _('Automatically routed fill stitching'), type='toggle', default=True, sort_index=1) def auto_fill2(self): - return self.get_boolean_param('auto_fill', True) - + return self.get_boolean_param('auto_fill', True) + @property - @param('fill_method', _('Fill method'), type='dropdown', default=0, options=[_("Auto Fill"), _("Tangential"), _("Guided Auto Fill")], sort_index = 2) + @param('fill_method', _('Fill method'), type='dropdown', default=0, + options=[_("Auto Fill"), _("Tangential"), _("Guided Auto Fill")], sort_index=2) def fill_method(self): return self.get_int_param('fill_method', 0) @property - @param('tangential_strategy', _('Tangential strategy'), type='dropdown', default=1, options=[_("Closest point"), _("Inner to Outer")],select_items=[('fill_method',1)], sort_index = 2) + @param('tangential_strategy', _('Tangential strategy'), type='dropdown', default=1, + options=[_("Closest point"), _("Inner to Outer")], select_items=[('fill_method', 1)], sort_index=2) def tangential_strategy(self): return self.get_int_param('tangential_strategy', 1) @property - @param('join_style', _('Join Style'), type='dropdown', default=0, options=[_("Round"), _("Mitered"), _("Beveled")],select_items=[('fill_method',1)], sort_index = 2) + @param('join_style', _('Join Style'), type='dropdown', default=0, + options=[_("Round"), _("Mitered"), _("Beveled")], select_items=[('fill_method', 1)], sort_index=2) def join_style(self): return self.get_int_param('join_style', 0) @property - @param('interlaced', _('Interlaced'), type='boolean', default=True,select_items=[('fill_method',1),('fill_method',2)], sort_index = 2) + @param('interlaced', _('Interlaced'), type='boolean', default=True, select_items=[('fill_method', 1), ('fill_method', 2)], sort_index=2) def interlaced(self): return self.get_boolean_param('interlaced', True) @property @param('angle', _('Angle of lines of stitches'), - tooltip=_('The angle increases in a counter-clockwise direction. 0 is horizontal. Negative angles are allowed.'), + tooltip=_( + 'The angle increases in a counter-clockwise direction. 0 is horizontal. Negative angles are allowed.'), unit='deg', type='float', - sort_index = 4, - select_items=[('fill_method',0)], + sort_index=4, + select_items=[('fill_method', 0)], default=0) @cache def angle(self): @@ -99,8 +102,8 @@ class AutoFill(EmbroideryElement): tooltip=_('The last stitch in each row is quite close to the first stitch in the next row. ' 'Skipping it decreases stitch count and density.'), type='boolean', - sort_index = 4, - select_items=[('fill_method',0), ('fill_method',2)], + sort_index=4, + select_items=[('fill_method', 0), ('fill_method', 2)], default=False) def skip_last(self): return self.get_boolean_param("skip_last", False) @@ -112,8 +115,8 @@ class AutoFill(EmbroideryElement): tooltip=_('The flip option can help you with routing your stitch path. ' 'When you enable flip, stitching goes from right-to-left instead of left-to-right.'), type='boolean', - sort_index = 4, - select_items=[('fill_method',0), ('fill_method',2)], + sort_index=4, + select_items=[('fill_method', 0), ('fill_method', 2)], default=False) def flip(self): return self.get_boolean_param("flip", False) @@ -123,7 +126,7 @@ class AutoFill(EmbroideryElement): _('Spacing between rows'), tooltip=_('Distance between rows of stitches.'), unit='mm', - sort_index = 4, + sort_index=4, type='float', default=0.25) def row_spacing(self): @@ -136,9 +139,10 @@ class AutoFill(EmbroideryElement): @property @param('max_stitch_length_mm', _('Maximum fill stitch length'), - tooltip=_('The length of each stitch in a row. Shorter stitch may be used at the start or end of a row.'), + tooltip=_( + 'The length of each stitch in a row. Shorter stitch may be used at the start or end of a row.'), unit='mm', - sort_index = 4, + sort_index=4, type='float', default=3.0) def max_stitch_length(self): @@ -147,10 +151,11 @@ class AutoFill(EmbroideryElement): @property @param('staggers', _('Stagger rows this many times before repeating'), - tooltip=_('Setting this dictates how many rows apart the stitches will be before they fall in the same column position.'), + tooltip=_( + 'Setting this dictates how many rows apart the stitches will be before they fall in the same column position.'), type='int', - sort_index = 4, - select_items=[('fill_method',0)], + sort_index=4, + select_items=[('fill_method', 0)], default=4) def staggers(self): return max(self.get_int_param("staggers", 4), 1) @@ -162,10 +167,10 @@ class AutoFill(EmbroideryElement): # ensure path length for i, path in enumerate(paths): if len(path) < 3: - paths[i] = [(path[0][0], path[0][1]), (path[0][0]+1.0, path[0][1]), (path[0][0], path[0][1]+1.0)] + paths[i] = [(path[0][0], path[0][1]), (path[0][0] + + 1.0, path[0][1]), (path[0][0], path[0][1]+1.0)] return paths - @property @cache def outline(self): @@ -176,19 +181,16 @@ class AutoFill(EmbroideryElement): def outline_length(self): return self.outline.length - @property - def flip(self): - return False - @property @param('running_stitch_length_mm', _('Running stitch length (traversal between sections)'), - tooltip=_('Length of stitches around the outline of the fill region used when moving from section to section.'), + tooltip=_( + 'Length of stitches around the outline of the fill region used when moving from section to section.'), unit='mm', type='float', default=1.5, - select_items=[('fill_method',0),('fill_method',2)], - sort_index = 4) + select_items=[('fill_method', 0), ('fill_method', 2)], + sort_index=4) def running_stitch_length(self): return max(self.get_float_param("running_stitch_length_mm", 1.5), 0.01) @@ -200,7 +202,8 @@ class AutoFill(EmbroideryElement): @property @param('fill_underlay_angle', _('Fill angle'), - tooltip=_('Default: fill angle + 90 deg. Insert comma-seperated list for multiple layers.'), + tooltip=_( + 'Default: fill angle + 90 deg. Insert comma-seperated list for multiple layers.'), unit='deg', group=_('AutoFill Underlay'), type='float') @@ -211,7 +214,8 @@ class AutoFill(EmbroideryElement): if underlay_angles is not None: underlay_angles = underlay_angles.strip().split(',') try: - underlay_angles = [math.radians(float(angle)) for angle in underlay_angles] + underlay_angles = [math.radians( + float(angle)) for angle in underlay_angles] except (TypeError, ValueError): return default_value else: @@ -243,7 +247,8 @@ class AutoFill(EmbroideryElement): @property @param('fill_underlay_inset_mm', _('Inset'), - tooltip=_('Shrink the shape before doing underlay, to prevent underlay from showing around the outside of the fill.'), + tooltip=_( + 'Shrink the shape before doing underlay, to prevent underlay from showing around the outside of the fill.'), unit='mm', group=_('AutoFill Underlay'), type='float', @@ -266,12 +271,13 @@ class AutoFill(EmbroideryElement): @property @param('expand_mm', _('Expand'), - tooltip=_('Expand the shape before fill stitching, to compensate for gaps between shapes.'), + tooltip=_( + 'Expand the shape before fill stitching, to compensate for gaps between shapes.'), unit='mm', type='float', default=0, - sort_index = 5, - select_items=[('fill_method',0),('fill_method',2)]) + sort_index=5, + select_items=[('fill_method', 0), ('fill_method', 2)]) def expand(self): return self.get_float_param('expand_mm', 0) @@ -283,8 +289,8 @@ class AutoFill(EmbroideryElement): 'are not visible. This gives them a jagged appearance.'), type='boolean', default=True, - select_items=[('fill_method',0),('fill_method',2)], - sort_index = 6) + select_items=[('fill_method', 0), ('fill_method', 2)], + sort_index=6) def underpath(self): return self.get_boolean_param('underpath', True) @@ -308,7 +314,8 @@ class AutoFill(EmbroideryElement): # from the first. So let's at least make sure the "first" thing is the # biggest path. paths = self.paths - paths.sort(key=lambda point_list: shgeo.Polygon(point_list).area, reverse=True) + paths.sort(key=lambda point_list: shgeo.Polygon( + point_list).area, reverse=True) # Very small holes will cause a shape to be rendered as an outline only # they are too small to be rendered and only confuse the auto_fill algorithm. # So let's ignore them @@ -397,7 +404,7 @@ class AutoFill(EmbroideryElement): color=self.color, tags=("auto_fill", "auto_fill_underlay"), stitches=auto_fill( - self.underlay_shape, + self.underlay_shape, None, self.fill_underlay_angle[i], self.fill_underlay_row_spacing, @@ -410,8 +417,8 @@ class AutoFill(EmbroideryElement): underpath=self.underlay_underpath)) stitch_groups.append(underlay) starting_point = underlay.stitches[-1] - - if self.fill_method == 0: #Auto Fill + + if self.fill_method == 0: # Auto Fill stitch_group = StitchGroup( color=self.color, tags=("auto_fill", "auto_fill_top"), @@ -429,30 +436,31 @@ class AutoFill(EmbroideryElement): ending_point, self.underpath)) stitch_groups.append(stitch_group) - elif self.fill_method == 1: #Tangential Fill + elif self.fill_method == 1: # Tangential Fill polygons = list(self.fill_shape) if not starting_point: - starting_point = (0,0) + starting_point = (0, 0) for poly in polygons: connectedLine, connectedLineOrigin = StitchPattern.offset_poly( - poly, - -self.row_spacing, - self.join_style+1, - self.max_stitch_length, + poly, + -self.row_spacing, + self.join_style+1, + self.max_stitch_length, self.interlaced, self.tangential_strategy, shgeo.Point(starting_point)) path = [InkstitchPoint(*p) for p in connectedLine] stitch_group = StitchGroup( - color=self.color, - tags=("auto_fill", "auto_fill_top"), - stitches=path) + color=self.color, + tags=("auto_fill", "auto_fill_top"), + stitches=path) stitch_groups.append(stitch_group) - elif self.fill_method == 2: #Guided Auto Fill - lines = get_patterns(self.node,"#inkstitch-guide-line-marker") + elif self.fill_method == 2: # Guided Auto Fill + lines = get_patterns(self.node, "#inkstitch-guide-line-marker") lines = lines['stroke_patterns'] if not lines or lines[0].is_empty: - inkex.errormsg(_("No line marked as guide line found within the same group as patch")) + inkex.errormsg( + _("No line marked as guide line found within the same group as patch")) else: stitch_group = StitchGroup( color=self.color, diff --git a/lib/elements/clone.py b/lib/elements/clone.py index bcecf3f0..15e7591c 100644 --- a/lib/elements/clone.py +++ b/lib/elements/clone.py @@ -14,7 +14,6 @@ from ..svg.tags import (EMBROIDERABLE_TAGS, INKSTITCH_ATTRIBS, from ..utils import cache from .auto_fill import AutoFill from .element import EmbroideryElement, param -#from .fill import Fill from .polyline import Polyline from .satin_column import SatinColumn from .stroke import Stroke @@ -79,9 +78,9 @@ class Clone(EmbroideryElement): else: elements = [] if element.get_style("fill", "black") and not element.get_style("stroke", 1) == "0": - #if element.get_boolean_param("auto_fill", True): + # if element.get_boolean_param("auto_fill", True): elements.append(AutoFill(node)) - #else: + # else: # elements.append(Fill(node)) if element.get_style("stroke", self.node) is not None: if not is_command(element.node): diff --git a/lib/elements/element.py b/lib/elements/element.py index b8728f60..ef70510d 100644 --- a/lib/elements/element.py +++ b/lib/elements/element.py @@ -33,7 +33,6 @@ class Param(object): self.tooltip = tooltip self.sort_index = sort_index self.select_items = select_items - #print("IN PARAM: ", self.values) def __repr__(self): return "Param(%s)" % vars(self) @@ -164,7 +163,8 @@ class EmbroideryElement(object): # Of course, transforms may also involve rotation, skewing, and translation. # All except translation can affect how wide the stroke appears on the screen. - node_transform = inkex.transforms.Transform(get_node_transform(self.node)) + node_transform = inkex.transforms.Transform( + get_node_transform(self.node)) # First, figure out the translation component of the transform. Using a zero # vector completely cancels out the rotation, scale, and skew components. @@ -198,7 +198,8 @@ class EmbroideryElement(object): @property @param('ties', _('Allow lock stitches'), - tooltip=_('Tie thread at the beginning and/or end of this object. Manual stitch will not add lock stitches.'), + tooltip=_( + 'Tie thread at the beginning and/or end of this object. Manual stitch will not add lock stitches.'), type='dropdown', # Ties: 0 = Both | 1 = Before | 2 = After | 3 = Neither # L10N options to allow lock stitch before and after objects @@ -256,7 +257,8 @@ class EmbroideryElement(object): d = self.node.get("d", "") if not d: - self.fatal(_("Object %(id)s has an empty 'd' attribute. Please delete this object from your document.") % dict(id=self.node.get("id"))) + self.fatal(_("Object %(id)s has an empty 'd' attribute. Please delete this object from your document.") % dict( + id=self.node.get("id"))) return inkex.paths.Path(d).to_superpath() @@ -266,7 +268,8 @@ class EmbroideryElement(object): @property def shape(self): - raise NotImplementedError("INTERNAL ERROR: %s must implement shape()", self.__class__) + raise NotImplementedError( + "INTERNAL ERROR: %s must implement shape()", self.__class__) @property @cache @@ -316,7 +319,8 @@ class EmbroideryElement(object): return self.get_boolean_param('stop_after', False) def to_stitch_groups(self, last_patch): - raise NotImplementedError("%s must implement to_stitch_groups()" % self.__class__.__name__) + raise NotImplementedError( + "%s must implement to_stitch_groups()" % self.__class__.__name__) def embroider(self, last_patch): self.validate() @@ -329,8 +333,10 @@ class EmbroideryElement(object): patch.force_lock_stitches = self.force_lock_stitches if patches: - patches[-1].trim_after = self.has_command("trim") or self.trim_after - patches[-1].stop_after = self.has_command("stop") or self.stop_after + patches[-1].trim_after = self.has_command( + "trim") or self.trim_after + patches[-1].stop_after = self.has_command( + "stop") or self.stop_after return patches diff --git a/lib/elements/utils.py b/lib/elements/utils.py index f858cc81..9fec8b63 100644 --- a/lib/elements/utils.py +++ b/lib/elements/utils.py @@ -11,7 +11,6 @@ from .auto_fill import AutoFill from .clone import Clone, is_clone from .element import EmbroideryElement from .empty_d_object import EmptyDObject -#from .fill import Fill from .image import ImageObject from .pattern import PatternObject from .polyline import Polyline @@ -41,9 +40,9 @@ def node_to_elements(node): # noqa: C901 else: elements = [] if element.get_style("fill", "black") and not element.get_style('fill-opacity', 1) == "0": - #if element.get_boolean_param("auto_fill", True): + # if element.get_boolean_param("auto_fill", True): elements.append(AutoFill(node)) - #else: + # else: # elements.append(Fill(node)) if element.get_style("stroke"): if not is_command(element.node): -- cgit v1.2.3 From 3caaae693893354ff10472044116e623e219e633 Mon Sep 17 00:00:00 2001 From: Andreas Date: Wed, 10 Nov 2021 17:23:24 +0100 Subject: bug fixing --- lib/elements/auto_fill.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'lib/elements') diff --git a/lib/elements/auto_fill.py b/lib/elements/auto_fill.py index 81abf7ad..094ad91e 100644 --- a/lib/elements/auto_fill.py +++ b/lib/elements/auto_fill.py @@ -456,7 +456,8 @@ class AutoFill(EmbroideryElement): stitches=path) stitch_groups.append(stitch_group) elif self.fill_method == 2: # Guided Auto Fill - lines = get_patterns(self.node, "#inkstitch-guide-line-marker") + lines = get_patterns( + self.node, "#inkstitch-guide-line-marker", False, True) lines = lines['stroke_patterns'] if not lines or lines[0].is_empty: inkex.errormsg( -- cgit v1.2.3 From d445b38629a902f6c13565a83ed81a91b6458480 Mon Sep 17 00:00:00 2001 From: Andreas Date: Sun, 21 Nov 2021 12:44:06 +0100 Subject: bug fixing+first spiral implementation --- lib/elements/auto_fill.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib/elements') diff --git a/lib/elements/auto_fill.py b/lib/elements/auto_fill.py index 094ad91e..dc678087 100644 --- a/lib/elements/auto_fill.py +++ b/lib/elements/auto_fill.py @@ -61,7 +61,7 @@ class AutoFill(EmbroideryElement): @property @param('tangential_strategy', _('Tangential strategy'), type='dropdown', default=1, - options=[_("Closest point"), _("Inner to Outer")], select_items=[('fill_method', 1)], sort_index=2) + options=[_("Closest point"), _("Inner to Outer"), _("single Spiral")], select_items=[('fill_method', 1)], sort_index=2) def tangential_strategy(self): return self.get_int_param('tangential_strategy', 1) -- cgit v1.2.3 From b5c7f637c14beaf9ba075c0c4445fbd3541a8270 Mon Sep 17 00:00:00 2001 From: Andreas Date: Wed, 24 Nov 2021 19:05:20 +0100 Subject: minor changes --- lib/elements/auto_fill.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib/elements') diff --git a/lib/elements/auto_fill.py b/lib/elements/auto_fill.py index dc678087..55c9e2d0 100644 --- a/lib/elements/auto_fill.py +++ b/lib/elements/auto_fill.py @@ -61,7 +61,7 @@ class AutoFill(EmbroideryElement): @property @param('tangential_strategy', _('Tangential strategy'), type='dropdown', default=1, - options=[_("Closest point"), _("Inner to Outer"), _("single Spiral")], select_items=[('fill_method', 1)], sort_index=2) + options=[_("Closest point"), _("Inner to Outer"), _("Single spiral")], select_items=[('fill_method', 1)], sort_index=2) def tangential_strategy(self): return self.get_int_param('tangential_strategy', 1) -- cgit v1.2.3 From 95a933161616e5860862435d4b999db49b8e50a5 Mon Sep 17 00:00:00 2001 From: Andreas Date: Fri, 28 Jan 2022 22:31:55 +0100 Subject: added legacy fill again --- lib/elements/auto_fill.py | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) (limited to 'lib/elements') diff --git a/lib/elements/auto_fill.py b/lib/elements/auto_fill.py index 55c9e2d0..3f75180b 100644 --- a/lib/elements/auto_fill.py +++ b/lib/elements/auto_fill.py @@ -14,7 +14,7 @@ from shapely import geometry as shgeo from shapely.validation import explain_validity from ..i18n import _ from ..stitch_plan import StitchGroup -from ..stitches import auto_fill +from ..stitches import auto_fill, fill from ..stitches import StitchPattern from ..utils import cache, version from .element import param @@ -55,7 +55,7 @@ class AutoFill(EmbroideryElement): @property @param('fill_method', _('Fill method'), type='dropdown', default=0, - options=[_("Auto Fill"), _("Tangential"), _("Guided Auto Fill")], sort_index=2) + options=[_("Auto Fill"), _("Tangential"), _("Guided Auto Fill"), _("Legacy Fill")], sort_index=2) def fill_method(self): return self.get_int_param('fill_method', 0) @@ -84,7 +84,7 @@ class AutoFill(EmbroideryElement): unit='deg', type='float', sort_index=4, - select_items=[('fill_method', 0)], + select_items=[('fill_method', 0), ('fill_method', 3)], default=0) @cache def angle(self): @@ -103,7 +103,8 @@ class AutoFill(EmbroideryElement): 'Skipping it decreases stitch count and density.'), type='boolean', sort_index=4, - select_items=[('fill_method', 0), ('fill_method', 2)], + select_items=[('fill_method', 0), ('fill_method', 2), + ('fill_method', 3)], default=False) def skip_last(self): return self.get_boolean_param("skip_last", False) @@ -116,7 +117,8 @@ class AutoFill(EmbroideryElement): 'When you enable flip, stitching goes from right-to-left instead of left-to-right.'), type='boolean', sort_index=4, - select_items=[('fill_method', 0), ('fill_method', 2)], + select_items=[('fill_method', 0), ('fill_method', 2), + ('fill_method', 3)], default=False) def flip(self): return self.get_boolean_param("flip", False) @@ -155,7 +157,7 @@ class AutoFill(EmbroideryElement): 'Setting this dictates how many rows apart the stitches will be before they fall in the same column position.'), type='int', sort_index=4, - select_items=[('fill_method', 0)], + select_items=[('fill_method', 0), ('fill_method', 3)], default=4) def staggers(self): return max(self.get_int_param("staggers", 4), 1) @@ -481,6 +483,21 @@ class AutoFill(EmbroideryElement): self.underpath, self.interlaced)) stitch_groups.append(stitch_group) + elif self.fill_method == 3: # Legacy Fill + stitch_lists = fill.legacy_fill(self.shape, + self.angle, + self.row_spacing, + self.end_row_spacing, + self.max_stitch_length, + self.flip, + self.staggers, + self.skip_last) + for stitch_list in stitch_lists: + stitch_group = StitchGroup( + color=self.color, + tags=("auto_fill", "auto_fill_top"), + stitches=stitch_list) + stitch_groups.append(stitch_group) except Exception: if hasattr(sys, 'gettrace') and sys.gettrace(): -- cgit v1.2.3 From 82216b184c669d6dea26672e5c0771146e62ca39 Mon Sep 17 00:00:00 2001 From: Kaalleen Date: Sat, 29 Jan 2022 09:53:50 +0100 Subject: remove some pattern and marker mixups and some style issues --- lib/elements/auto_fill.py | 29 ++++++++++++++--------------- lib/elements/utils.py | 4 ++-- 2 files changed, 16 insertions(+), 17 deletions(-) (limited to 'lib/elements') diff --git a/lib/elements/auto_fill.py b/lib/elements/auto_fill.py index 3f75180b..614e6887 100644 --- a/lib/elements/auto_fill.py +++ b/lib/elements/auto_fill.py @@ -3,27 +3,26 @@ # Copyright (c) 2010 Authors # Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. +import logging import math +import re import sys import traceback -import re -import logging -import inkex +import inkex from shapely import geometry as shgeo from shapely.validation import explain_validity + from ..i18n import _ +from ..marker import get_marker_elements from ..stitch_plan import StitchGroup -from ..stitches import auto_fill, fill -from ..stitches import StitchPattern -from ..utils import cache, version -from .element import param -from .element import EmbroideryElement -from ..patterns import get_patterns -from .validation import ValidationWarning -from ..utils import Point as InkstitchPoint +from ..stitches import StitchPattern, auto_fill, fill from ..svg import PIXELS_PER_MM from ..svg.tags import INKSCAPE_LABEL +from ..utils import Point as InkstitchPoint +from ..utils import cache, version +from .element import EmbroideryElement, param +from .validation import ValidationWarning class SmallShapeWarning(ValidationWarning): @@ -393,7 +392,8 @@ class AutoFill(EmbroideryElement): else: return None - def to_stitch_groups(self, last_patch): + def to_stitch_groups(self, last_patch): # noqa: C901 + # TODO: split this up do_legacy_fill() etc. stitch_groups = [] starting_point = self.get_starting_point(last_patch) @@ -458,9 +458,8 @@ class AutoFill(EmbroideryElement): stitches=path) stitch_groups.append(stitch_group) elif self.fill_method == 2: # Guided Auto Fill - lines = get_patterns( - self.node, "#inkstitch-guide-line-marker", False, True) - lines = lines['stroke_patterns'] + lines = get_marker_elements(self.node, "guide-line", False, True) + lines = lines['stroke'] if not lines or lines[0].is_empty: inkex.errormsg( _("No line marked as guide line found within the same group as patch")) diff --git a/lib/elements/utils.py b/lib/elements/utils.py index 9fec8b63..9b9b8f14 100644 --- a/lib/elements/utils.py +++ b/lib/elements/utils.py @@ -4,7 +4,7 @@ # Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. from ..commands import is_command -from ..patterns import is_pattern +from ..marker import has_marker from ..svg.tags import (EMBROIDERABLE_TAGS, SVG_IMAGE_TAG, SVG_PATH_TAG, SVG_POLYLINE_TAG, SVG_TEXT_TAG) from .auto_fill import AutoFill @@ -29,7 +29,7 @@ def node_to_elements(node): # noqa: C901 elif node.tag == SVG_PATH_TAG and not node.get('d', ''): return [EmptyDObject(node)] - elif is_pattern(node): + elif has_marker(node, 'pattern'): return [PatternObject(node)] elif node.tag in EMBROIDERABLE_TAGS: -- cgit v1.2.3 From 3d1600ed039c9078bcb4a28328ab60eb96994dfd Mon Sep 17 00:00:00 2001 From: Kaalleen Date: Sun, 30 Jan 2022 15:48:51 +0100 Subject: * autofill to fillstitch * remove too complex warning for fillstitch * some marker adjustments --- lib/elements/__init__.py | 3 +- lib/elements/auto_fill.py | 533 ------------------------------------- lib/elements/clone.py | 38 +-- lib/elements/element.py | 12 +- lib/elements/fill_stitch.py | 624 ++++++++++++++++++++++++++++++++++++++++++++ lib/elements/marker.py | 32 +++ lib/elements/pattern.py | 33 --- lib/elements/utils.py | 13 +- 8 files changed, 682 insertions(+), 606 deletions(-) delete mode 100644 lib/elements/auto_fill.py create mode 100644 lib/elements/fill_stitch.py create mode 100644 lib/elements/marker.py delete mode 100644 lib/elements/pattern.py (limited to 'lib/elements') diff --git a/lib/elements/__init__.py b/lib/elements/__init__.py index bb5c95ba..00933f36 100644 --- a/lib/elements/__init__.py +++ b/lib/elements/__init__.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 .auto_fill import AutoFill from .clone import Clone from .element import EmbroideryElement from .empty_d_object import EmptyDObject -#from .fill import Fill +from .fill_stitch import FillStitch from .image import ImageObject from .polyline import Polyline from .satin_column import SatinColumn diff --git a/lib/elements/auto_fill.py b/lib/elements/auto_fill.py deleted file mode 100644 index 614e6887..00000000 --- a/lib/elements/auto_fill.py +++ /dev/null @@ -1,533 +0,0 @@ -# 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 logging -import math -import re -import sys -import traceback - -import inkex -from shapely import geometry as shgeo -from shapely.validation import explain_validity - -from ..i18n import _ -from ..marker import get_marker_elements -from ..stitch_plan import StitchGroup -from ..stitches import StitchPattern, auto_fill, fill -from ..svg import PIXELS_PER_MM -from ..svg.tags import INKSCAPE_LABEL -from ..utils import Point as InkstitchPoint -from ..utils import cache, version -from .element import EmbroideryElement, param -from .validation import ValidationWarning - - -class SmallShapeWarning(ValidationWarning): - name = _("Small Fill") - description = _("This fill object is so small that it would probably look better as running stitch or satin column. " - "For very small shapes, fill stitch is not possible, and Ink/Stitch will use running stitch around " - "the outline instead.") - - -class ExpandWarning(ValidationWarning): - name = _("Expand") - description = _("The expand parameter for this fill object cannot be applied. " - "Ink/Stitch will ignore it and will use original size instead.") - - -class UnderlayInsetWarning(ValidationWarning): - name = _("Inset") - description = _("The underlay inset parameter for this fill object cannot be applied. " - "Ink/Stitch will ignore it and will use the original size instead.") - - -class AutoFill(EmbroideryElement): - element_name = _("AutoFill") - - @property - @param('auto_fill', _('Automatically routed fill stitching'), type='toggle', default=True, sort_index=1) - def auto_fill2(self): - return self.get_boolean_param('auto_fill', True) - - @property - @param('fill_method', _('Fill method'), type='dropdown', default=0, - options=[_("Auto Fill"), _("Tangential"), _("Guided Auto Fill"), _("Legacy Fill")], sort_index=2) - def fill_method(self): - return self.get_int_param('fill_method', 0) - - @property - @param('tangential_strategy', _('Tangential strategy'), type='dropdown', default=1, - options=[_("Closest point"), _("Inner to Outer"), _("Single spiral")], select_items=[('fill_method', 1)], sort_index=2) - def tangential_strategy(self): - return self.get_int_param('tangential_strategy', 1) - - @property - @param('join_style', _('Join Style'), type='dropdown', default=0, - options=[_("Round"), _("Mitered"), _("Beveled")], select_items=[('fill_method', 1)], sort_index=2) - def join_style(self): - return self.get_int_param('join_style', 0) - - @property - @param('interlaced', _('Interlaced'), type='boolean', default=True, select_items=[('fill_method', 1), ('fill_method', 2)], sort_index=2) - def interlaced(self): - return self.get_boolean_param('interlaced', True) - - @property - @param('angle', - _('Angle of lines of stitches'), - tooltip=_( - 'The angle increases in a counter-clockwise direction. 0 is horizontal. Negative angles are allowed.'), - unit='deg', - type='float', - sort_index=4, - select_items=[('fill_method', 0), ('fill_method', 3)], - default=0) - @cache - def angle(self): - return math.radians(self.get_float_param('angle', 0)) - - @property - def color(self): - # SVG spec says the default fill is black - return self.get_style("fill", "#000000") - - @property - @param( - 'skip_last', - _('Skip last stitch in each row'), - tooltip=_('The last stitch in each row is quite close to the first stitch in the next row. ' - 'Skipping it decreases stitch count and density.'), - type='boolean', - sort_index=4, - select_items=[('fill_method', 0), ('fill_method', 2), - ('fill_method', 3)], - default=False) - def skip_last(self): - return self.get_boolean_param("skip_last", False) - - @property - @param( - 'flip', - _('Flip fill (start right-to-left)'), - tooltip=_('The flip option can help you with routing your stitch path. ' - 'When you enable flip, stitching goes from right-to-left instead of left-to-right.'), - type='boolean', - sort_index=4, - select_items=[('fill_method', 0), ('fill_method', 2), - ('fill_method', 3)], - default=False) - def flip(self): - return self.get_boolean_param("flip", False) - - @property - @param('row_spacing_mm', - _('Spacing between rows'), - tooltip=_('Distance between rows of stitches.'), - unit='mm', - sort_index=4, - type='float', - default=0.25) - def row_spacing(self): - return max(self.get_float_param("row_spacing_mm", 0.25), 0.1 * PIXELS_PER_MM) - - @property - def end_row_spacing(self): - return self.get_float_param("end_row_spacing_mm") - - @property - @param('max_stitch_length_mm', - _('Maximum fill stitch length'), - tooltip=_( - 'The length of each stitch in a row. Shorter stitch may be used at the start or end of a row.'), - unit='mm', - sort_index=4, - type='float', - default=3.0) - def max_stitch_length(self): - return max(self.get_float_param("max_stitch_length_mm", 3.0), 0.1 * PIXELS_PER_MM) - - @property - @param('staggers', - _('Stagger rows this many times before repeating'), - tooltip=_( - 'Setting this dictates how many rows apart the stitches will be before they fall in the same column position.'), - type='int', - sort_index=4, - select_items=[('fill_method', 0), ('fill_method', 3)], - default=4) - def staggers(self): - return max(self.get_int_param("staggers", 4), 1) - - @property - @cache - def paths(self): - paths = self.flatten(self.parse_path()) - # ensure path length - for i, path in enumerate(paths): - if len(path) < 3: - paths[i] = [(path[0][0], path[0][1]), (path[0][0] + - 1.0, path[0][1]), (path[0][0], path[0][1]+1.0)] - return paths - - @property - @cache - def outline(self): - return self.shape.boundary[0] - - @property - @cache - def outline_length(self): - return self.outline.length - - @property - @param('running_stitch_length_mm', - _('Running stitch length (traversal between sections)'), - tooltip=_( - 'Length of stitches around the outline of the fill region used when moving from section to section.'), - unit='mm', - type='float', - default=1.5, - select_items=[('fill_method', 0), ('fill_method', 2)], - sort_index=4) - def running_stitch_length(self): - return max(self.get_float_param("running_stitch_length_mm", 1.5), 0.01) - - @property - @param('fill_underlay', _('Underlay'), type='toggle', group=_('AutoFill Underlay'), default=True) - def fill_underlay(self): - return self.get_boolean_param("fill_underlay", default=True) - - @property - @param('fill_underlay_angle', - _('Fill angle'), - tooltip=_( - 'Default: fill angle + 90 deg. Insert comma-seperated list for multiple layers.'), - unit='deg', - group=_('AutoFill Underlay'), - type='float') - @cache - def fill_underlay_angle(self): - underlay_angles = self.get_param('fill_underlay_angle', None) - default_value = [self.angle + math.pi / 2.0] - if underlay_angles is not None: - underlay_angles = underlay_angles.strip().split(',') - try: - underlay_angles = [math.radians( - float(angle)) for angle in underlay_angles] - except (TypeError, ValueError): - return default_value - else: - underlay_angles = default_value - - return underlay_angles - - @property - @param('fill_underlay_row_spacing_mm', - _('Row spacing'), - tooltip=_('default: 3x fill row spacing'), - unit='mm', - group=_('AutoFill Underlay'), - type='float') - @cache - def fill_underlay_row_spacing(self): - return self.get_float_param("fill_underlay_row_spacing_mm") or self.row_spacing * 3 - - @property - @param('fill_underlay_max_stitch_length_mm', - _('Max stitch length'), - tooltip=_('default: equal to fill max stitch length'), - unit='mm', - group=_('AutoFill Underlay'), type='float') - @cache - def fill_underlay_max_stitch_length(self): - return self.get_float_param("fill_underlay_max_stitch_length_mm") or self.max_stitch_length - - @property - @param('fill_underlay_inset_mm', - _('Inset'), - tooltip=_( - 'Shrink the shape before doing underlay, to prevent underlay from showing around the outside of the fill.'), - unit='mm', - group=_('AutoFill Underlay'), - type='float', - default=0) - def fill_underlay_inset(self): - return self.get_float_param('fill_underlay_inset_mm', 0) - - @property - @param( - 'fill_underlay_skip_last', - _('Skip last stitch in each row'), - tooltip=_('The last stitch in each row is quite close to the first stitch in the next row. ' - 'Skipping it decreases stitch count and density.'), - group=_('AutoFill Underlay'), - type='boolean', - default=False) - def fill_underlay_skip_last(self): - return self.get_boolean_param("fill_underlay_skip_last", False) - - @property - @param('expand_mm', - _('Expand'), - tooltip=_( - 'Expand the shape before fill stitching, to compensate for gaps between shapes.'), - unit='mm', - type='float', - default=0, - sort_index=5, - select_items=[('fill_method', 0), ('fill_method', 2)]) - def expand(self): - return self.get_float_param('expand_mm', 0) - - @property - @param('underpath', - _('Underpath'), - tooltip=_('Travel inside the shape when moving from section to section. Underpath ' - 'stitches avoid traveling in the direction of the row angle so that they ' - 'are not visible. This gives them a jagged appearance.'), - type='boolean', - default=True, - select_items=[('fill_method', 0), ('fill_method', 2)], - sort_index=6) - def underpath(self): - return self.get_boolean_param('underpath', True) - - @property - @param( - 'underlay_underpath', - _('Underpath'), - tooltip=_('Travel inside the shape when moving from section to section. Underpath ' - 'stitches avoid traveling in the direction of the row angle so that they ' - 'are not visible. This gives them a jagged appearance.'), - group=_('AutoFill Underlay'), - type='boolean', - default=True) - def underlay_underpath(self): - return self.get_boolean_param('underlay_underpath', True) - - @property - @cache - def shape(self): - # shapely's idea of "holes" are to subtract everything in the second set - # from the first. So let's at least make sure the "first" thing is the - # biggest path. - paths = self.paths - paths.sort(key=lambda point_list: shgeo.Polygon( - point_list).area, reverse=True) - # Very small holes will cause a shape to be rendered as an outline only - # they are too small to be rendered and only confuse the auto_fill algorithm. - # So let's ignore them - if shgeo.Polygon(paths[0]).area > 5 and shgeo.Polygon(paths[-1]).area < 5: - paths = [path for path in paths if shgeo.Polygon(path).area > 3] - - polygon = shgeo.MultiPolygon([(paths[0], paths[1:])]) - - # There is a great number of "crossing border" errors on fill shapes - # If the polygon fails, we can try to run buffer(0) on the polygon in the - # hope it will fix at least some of them - if not self.shape_is_valid(polygon): - why = explain_validity(polygon) - message = re.match(r".+?(?=\[)", why) - if message.group(0) == "Self-intersection": - buffered = polygon.buffer(0) - # we do not want to break apart into multiple objects (possibly in the future?!) - # best way to distinguish the resulting polygon is to compare the area size of the two - # and make sure users will not experience significantly altered shapes without a warning - if math.isclose(polygon.area, buffered.area): - polygon = shgeo.MultiPolygon([buffered]) - - return polygon - - def shape_is_valid(self, shape): - # Shapely will log to stdout to complain about the shape unless we make - # it shut up. - logger = logging.getLogger('shapely.geos') - level = logger.level - logger.setLevel(logging.CRITICAL) - - valid = shape.is_valid - - logger.setLevel(level) - - return valid - - def shrink_or_grow_shape(self, amount, validate=False): - if amount: - shape = self.shape.buffer(amount) - # changing the size can empty the shape - # in this case we want to use the original shape rather than returning an error - if shape.is_empty and not validate: - return self.shape - if not isinstance(shape, shgeo.MultiPolygon): - shape = shgeo.MultiPolygon([shape]) - return shape - else: - return self.shape - - @property - def underlay_shape(self): - return self.shrink_or_grow_shape(-self.fill_underlay_inset) - - @property - def fill_shape(self): - return self.shrink_or_grow_shape(self.expand) - - def get_starting_point(self, last_patch): - # If there is a "fill_start" Command, then use that; otherwise pick - # the point closest to the end of the last patch. - - if self.get_command('fill_start'): - return self.get_command('fill_start').target_point - elif last_patch: - return last_patch.stitches[-1] - else: - return None - - def get_ending_point(self): - if self.get_command('fill_end'): - return self.get_command('fill_end').target_point - else: - return None - - def to_stitch_groups(self, last_patch): # noqa: C901 - # TODO: split this up do_legacy_fill() etc. - stitch_groups = [] - - starting_point = self.get_starting_point(last_patch) - ending_point = self.get_ending_point() - - try: - if self.fill_underlay: - for i in range(len(self.fill_underlay_angle)): - underlay = StitchGroup( - color=self.color, - tags=("auto_fill", "auto_fill_underlay"), - stitches=auto_fill( - self.underlay_shape, - None, - self.fill_underlay_angle[i], - self.fill_underlay_row_spacing, - self.fill_underlay_row_spacing, - self.fill_underlay_max_stitch_length, - self.running_stitch_length, - self.staggers, - self.fill_underlay_skip_last, - starting_point, - underpath=self.underlay_underpath)) - stitch_groups.append(underlay) - starting_point = underlay.stitches[-1] - - if self.fill_method == 0: # Auto Fill - stitch_group = StitchGroup( - color=self.color, - tags=("auto_fill", "auto_fill_top"), - stitches=auto_fill( - self.fill_shape, - None, - self.angle, - self.row_spacing, - self.end_row_spacing, - self.max_stitch_length, - self.running_stitch_length, - self.staggers, - self.skip_last, - starting_point, - ending_point, - self.underpath)) - stitch_groups.append(stitch_group) - elif self.fill_method == 1: # Tangential Fill - polygons = list(self.fill_shape) - if not starting_point: - starting_point = (0, 0) - for poly in polygons: - connectedLine, connectedLineOrigin = StitchPattern.offset_poly( - poly, - -self.row_spacing, - self.join_style+1, - self.max_stitch_length, - self.interlaced, - self.tangential_strategy, - shgeo.Point(starting_point)) - path = [InkstitchPoint(*p) for p in connectedLine] - stitch_group = StitchGroup( - color=self.color, - tags=("auto_fill", "auto_fill_top"), - stitches=path) - stitch_groups.append(stitch_group) - elif self.fill_method == 2: # Guided Auto Fill - lines = get_marker_elements(self.node, "guide-line", False, True) - lines = lines['stroke'] - if not lines or lines[0].is_empty: - inkex.errormsg( - _("No line marked as guide line found within the same group as patch")) - else: - stitch_group = StitchGroup( - color=self.color, - tags=("auto_fill", "auto_fill_top"), - stitches=auto_fill( - self.fill_shape, - lines[0].geoms[0], - self.angle, - self.row_spacing, - self.end_row_spacing, - self.max_stitch_length, - self.running_stitch_length, - 0, - self.skip_last, - starting_point, - ending_point, - self.underpath, - self.interlaced)) - stitch_groups.append(stitch_group) - elif self.fill_method == 3: # Legacy Fill - stitch_lists = fill.legacy_fill(self.shape, - self.angle, - self.row_spacing, - self.end_row_spacing, - self.max_stitch_length, - self.flip, - self.staggers, - self.skip_last) - for stitch_list in stitch_lists: - stitch_group = StitchGroup( - color=self.color, - tags=("auto_fill", "auto_fill_top"), - stitches=stitch_list) - stitch_groups.append(stitch_group) - - except Exception: - if hasattr(sys, 'gettrace') and sys.gettrace(): - # if we're debugging, let the exception bubble up - raise - - # for an uncaught exception, give a little more info so that they can create a bug report - message = "" - message += _("Error during autofill! This means that there is a problem with Ink/Stitch.") - message += "\n\n" - # L10N this message is followed by a URL: https://github.com/inkstitch/inkstitch/issues/new - message += _("If you'd like to help us make Ink/Stitch better, please paste this whole message into a new issue at: ") - message += "https://github.com/inkstitch/inkstitch/issues/new\n\n" - message += version.get_inkstitch_version() + "\n\n" - message += traceback.format_exc() - - self.fatal(message) - - return stitch_groups - - -def validation_warnings(self): - if self.shape.area < 20: - label = self.node.get(INKSCAPE_LABEL) or self.node.get("id") - yield SmallShapeWarning(self.shape.centroid, label) - - if self.shrink_or_grow_shape(self.expand, True).is_empty: - yield ExpandWarning(self.shape.centroid) - - if self.shrink_or_grow_shape(-self.fill_underlay_inset, True).is_empty: - yield UnderlayInsetWarning(self.shape.centroid) - - for warning in super(AutoFill, self).validation_warnings(): - yield warning diff --git a/lib/elements/clone.py b/lib/elements/clone.py index 15e7591c..3f133471 100644 --- a/lib/elements/clone.py +++ b/lib/elements/clone.py @@ -5,18 +5,20 @@ from math import atan, degrees +<<<<<<< HEAD from ..commands import is_command, is_command_symbol +======= +import inkex + +from ..commands import is_command_symbol +>>>>>>> c69b6f5a (* autofill to fillstitch) from ..i18n import _ from ..svg.path import get_node_transform from ..svg.svg import find_elements -from ..svg.tags import (EMBROIDERABLE_TAGS, INKSTITCH_ATTRIBS, - SVG_POLYLINE_TAG, SVG_USE_TAG, XLINK_HREF) +from ..svg.tags import (EMBROIDERABLE_TAGS, INKSTITCH_ATTRIBS, SVG_USE_TAG, + XLINK_HREF) from ..utils import cache -from .auto_fill import AutoFill from .element import EmbroideryElement, param -from .polyline import Polyline -from .satin_column import SatinColumn -from .stroke import Stroke from .validation import ObjectTypeWarning, ValidationWarning @@ -67,28 +69,8 @@ class Clone(EmbroideryElement): return self.get_float_param('angle', 0) def clone_to_element(self, node): - # we need to determine if the source element is polyline, stroke, fill or satin - element = EmbroideryElement(node) - - if node.tag == SVG_POLYLINE_TAG: - return [Polyline(node)] - - elif element.get_boolean_param("satin_column") and self.get_clone_style("stroke", self.node): - return [SatinColumn(node)] - else: - elements = [] - if element.get_style("fill", "black") and not element.get_style("stroke", 1) == "0": - # if element.get_boolean_param("auto_fill", True): - elements.append(AutoFill(node)) - # else: - # elements.append(Fill(node)) - if element.get_style("stroke", self.node) is not None: - if not is_command(element.node): - elements.append(Stroke(node)) - if element.get_boolean_param("stroke_first", False): - elements.reverse() - - return elements + from .utils import node_to_elements + return node_to_elements(node) def to_stitch_groups(self, last_patch=None): patches = [] diff --git a/lib/elements/element.py b/lib/elements/element.py index ef70510d..ee4eadbb 100644 --- a/lib/elements/element.py +++ b/lib/elements/element.py @@ -87,8 +87,11 @@ class EmbroideryElement(object): return params def replace_legacy_param(self, param): - value = self.node.get(param, "").strip() - self.set_param(param[10:], value) + # remove "embroider_" prefix + new_param = param[10:] + if new_param in INKSTITCH_ATTRIBS: + value = self.node.get(param, "").strip() + self.set_param(param[10:], value) del self.node.attrib[param] @cache @@ -266,6 +269,11 @@ class EmbroideryElement(object): def parse_path(self): return apply_transforms(self.path, self.node) + @property + @cache + def paths(self): + return self.flatten(self.parse_path()) + @property def shape(self): raise NotImplementedError( diff --git a/lib/elements/fill_stitch.py b/lib/elements/fill_stitch.py new file mode 100644 index 00000000..ee56abfc --- /dev/null +++ b/lib/elements/fill_stitch.py @@ -0,0 +1,624 @@ +# 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 logging +import math +import re +import sys +import traceback + +from shapely import geometry as shgeo +from shapely.validation import explain_validity + +from ..i18n import _ +from ..marker import get_marker_elements +from ..stitch_plan import StitchGroup +from ..stitches import StitchPattern, auto_fill, legacy_fill +from ..svg import PIXELS_PER_MM +from ..svg.tags import INKSCAPE_LABEL +from ..utils import Point as InkstitchPoint +from ..utils import cache, version +from .element import EmbroideryElement, param +from .validation import ValidationError, ValidationWarning +from shapely.ops import nearest_points + + +class SmallShapeWarning(ValidationWarning): + name = _("Small Fill") + description = _("This fill object is so small that it would probably look better as running stitch or satin column. " + "For very small shapes, fill stitch is not possible, and Ink/Stitch will use running stitch around " + "the outline instead.") + + +class ExpandWarning(ValidationWarning): + name = _("Expand") + description = _("The expand parameter for this fill object cannot be applied. " + "Ink/Stitch will ignore it and will use original size instead.") + + +class UnderlayInsetWarning(ValidationWarning): + name = _("Inset") + description = _("The underlay inset parameter for this fill object cannot be applied. " + "Ink/Stitch will ignore it and will use the original size instead.") + +class MissingGuideLineWarning(ValidationWarning): + name = _("Missing Guideline") + description = _('This object is set to "Guided AutoFill", but has no guide line.') + steps_to_solve = [ + _('* Create a stroke object'), + _('* Select this object and run Extensions > Ink/Stitch > Edit > Selection to guide line') + ] + +class DisjointGuideLineWarning(ValidationWarning): + name = _("Disjointed Guide Line") + description = _("The guide line of this object isn't within the object borders. " + "The guide line works best, if it is within the target element.") + steps_to_solve = [ + _('* Move the guide line into the element') + ] + +class MultipleGuideLineWarning(ValidationWarning): + name = _("Multiple Guide Lines") + description = _("This object has multiple guide lines, but only the first one will be used.") + steps_to_solve = [ + _("* Remove all guide lines, except for one.") + ] + +class UnconnectedError(ValidationError): + name = _("Unconnected") + description = _("Fill: This object is made up of unconnected shapes. This is not allowed because " + "Ink/Stitch doesn't know what order to stitch them in. Please break this " + "object up into separate shapes.") + steps_to_solve = [ + _('* Extensions > Ink/Stitch > Fill Tools > Break Apart Fill Objects'), + ] + + +class InvalidShapeError(ValidationError): + name = _("Border crosses itself") + description = _("Fill: Shape is not valid. This can happen if the border crosses over itself.") + steps_to_solve = [ + _('* Extensions > Ink/Stitch > Fill Tools > Break Apart Fill Objects') + ] + + +class FillStitch(EmbroideryElement): + element_name = _("FillStitch") + + @property + @param('auto_fill', _('Automatically routed fill stitching'), type='toggle', default=True, sort_index=1) + def auto_fill(self): + return self.get_boolean_param('auto_fill', True) + + @property + @param('fill_method', _('Fill method'), type='dropdown', default=0, + options=[_("Auto Fill"), _("Tangential"), _("Guided Auto Fill"), _("Legacy Fill")], sort_index=2) + def fill_method(self): + return self.get_int_param('fill_method', 0) + + @property + @param('tangential_strategy', _('Tangential strategy'), type='dropdown', default=1, + options=[_("Closest point"), _("Inner to Outer"), _("Single spiral")], select_items=[('fill_method', 1)], sort_index=2) + def tangential_strategy(self): + return self.get_int_param('tangential_strategy', 1) + + @property + @param('join_style', _('Join Style'), type='dropdown', default=0, + options=[_("Round"), _("Mitered"), _("Beveled")], select_items=[('fill_method', 1)], sort_index=2) + def join_style(self): + return self.get_int_param('join_style', 0) + + @property + @param('interlaced', _('Interlaced'), type='boolean', default=True, select_items=[('fill_method', 1), ('fill_method', 2)], sort_index=2) + def interlaced(self): + return self.get_boolean_param('interlaced', True) + + @property + @param('angle', + _('Angle of lines of stitches'), + tooltip=_( + 'The angle increases in a counter-clockwise direction. 0 is horizontal. Negative angles are allowed.'), + unit='deg', + type='float', + sort_index=4, + select_items=[('fill_method', 0), ('fill_method', 3)], + default=0) + @cache + def angle(self): + return math.radians(self.get_float_param('angle', 0)) + + @property + def color(self): + # SVG spec says the default fill is black + return self.get_style("fill", "#000000") + + @property + @param( + 'skip_last', + _('Skip last stitch in each row'), + tooltip=_('The last stitch in each row is quite close to the first stitch in the next row. ' + 'Skipping it decreases stitch count and density.'), + type='boolean', + sort_index=4, + select_items=[('fill_method', 0), ('fill_method', 2), + ('fill_method', 3)], + default=False) + def skip_last(self): + return self.get_boolean_param("skip_last", False) + + @property + @param( + 'flip', + _('Flip fill (start right-to-left)'), + tooltip=_('The flip option can help you with routing your stitch path. ' + 'When you enable flip, stitching goes from right-to-left instead of left-to-right.'), + type='boolean', + sort_index=4, + select_items=[('fill_method', 0), ('fill_method', 2), + ('fill_method', 3)], + default=False) + def flip(self): + return self.get_boolean_param("flip", False) + + @property + @param('row_spacing_mm', + _('Spacing between rows'), + tooltip=_('Distance between rows of stitches.'), + unit='mm', + sort_index=4, + type='float', + default=0.25) + def row_spacing(self): + return max(self.get_float_param("row_spacing_mm", 0.25), 0.1 * PIXELS_PER_MM) + + @property + def end_row_spacing(self): + return self.get_float_param("end_row_spacing_mm") + + @property + @param('max_stitch_length_mm', + _('Maximum fill stitch length'), + tooltip=_( + 'The length of each stitch in a row. Shorter stitch may be used at the start or end of a row.'), + unit='mm', + sort_index=4, + type='float', + default=3.0) + def max_stitch_length(self): + return max(self.get_float_param("max_stitch_length_mm", 3.0), 0.1 * PIXELS_PER_MM) + + @property + @param('staggers', + _('Stagger rows this many times before repeating'), + tooltip=_( + 'Setting this dictates how many rows apart the stitches will be before they fall in the same column position.'), + type='int', + sort_index=4, + select_items=[('fill_method', 0), ('fill_method', 3)], + default=4) + def staggers(self): + return max(self.get_int_param("staggers", 4), 1) + + @property + @cache + def paths(self): + paths = self.flatten(self.parse_path()) + # ensure path length + for i, path in enumerate(paths): + if len(path) < 3: + paths[i] = [(path[0][0], path[0][1]), (path[0][0]+1.0, path[0][1]), (path[0][0], path[0][1]+1.0)] + return paths + + @property + @cache + def shape(self): + # shapely's idea of "holes" are to subtract everything in the second set + # from the first. So let's at least make sure the "first" thing is the + # biggest path. + paths = self.paths + paths.sort(key=lambda point_list: shgeo.Polygon(point_list).area, reverse=True) + # Very small holes will cause a shape to be rendered as an outline only + # they are too small to be rendered and only confuse the auto_fill algorithm. + # So let's ignore them + if shgeo.Polygon(paths[0]).area > 5 and shgeo.Polygon(paths[-1]).area < 5: + paths = [path for path in paths if shgeo.Polygon(path).area > 3] + + polygon = shgeo.MultiPolygon([(paths[0], paths[1:])]) + + # There is a great number of "crossing border" errors on fill shapes + # If the polygon fails, we can try to run buffer(0) on the polygon in the + # hope it will fix at least some of them + if not self.shape_is_valid(polygon): + why = explain_validity(polygon) + message = re.match(r".+?(?=\[)", why) + if message.group(0) == "Self-intersection": + buffered = polygon.buffer(0) + # if we receive a multipolygon, only use the first one of it + if type(buffered) == shgeo.MultiPolygon: + buffered = buffered[0] + # we do not want to break apart into multiple objects (possibly in the future?!) + # best way to distinguish the resulting polygon is to compare the area size of the two + # and make sure users will not experience significantly altered shapes without a warning + if type(buffered) == shgeo.Polygon and math.isclose(polygon.area, buffered.area, abs_tol=0.5): + polygon = shgeo.MultiPolygon([buffered]) + + return polygon + + def shape_is_valid(self, shape): + # Shapely will log to stdout to complain about the shape unless we make + # it shut up. + logger = logging.getLogger('shapely.geos') + level = logger.level + logger.setLevel(logging.CRITICAL) + + valid = shape.is_valid + + logger.setLevel(level) + + return valid + + def validation_errors(self): + if not self.shape_is_valid(self.shape): + why = explain_validity(self.shape) + message, x, y = re.findall(r".+?(?=\[)|-?\d+(?:\.\d+)?", why) + + # I Wish this weren't so brittle... + if "Hole lies outside shell" in message: + yield UnconnectedError((x, y)) + else: + yield InvalidShapeError((x, y)) + + def validation_warnings(self): + if self.shape.area < 20: + label = self.node.get(INKSCAPE_LABEL) or self.node.get("id") + yield SmallShapeWarning(self.shape.centroid, label) + + if self.shrink_or_grow_shape(self.expand, True).is_empty: + yield ExpandWarning(self.shape.centroid) + + if self.shrink_or_grow_shape(-self.fill_underlay_inset, True).is_empty: + yield UnderlayInsetWarning(self.shape.centroid) + + # guided fill warnings + if self.fill_method == 2: + guide_lines = self._get_guide_lines(True) + if not guide_lines or guide_lines[0].is_empty: + yield MissingGuideLineWarning(self.shape.centroid) + elif len(guide_lines) > 1: + yield MultipleGuideLineWarning(self.shape.centroid) + elif guide_lines[0].disjoint(self.shape): + yield DisjointGuideLineWarning(self.shape.centroid) + return None + + for warning in super(FillStitch, self).validation_warnings(): + yield warning + + @property + @cache + def outline(self): + return self.shape.boundary[0] + + @property + @cache + def outline_length(self): + return self.outline.length + + @property + @param('running_stitch_length_mm', + _('Running stitch length (traversal between sections)'), + tooltip=_( + 'Length of stitches around the outline of the fill region used when moving from section to section.'), + unit='mm', + type='float', + default=1.5, + select_items=[('fill_method', 0), ('fill_method', 2)], + sort_index=4) + def running_stitch_length(self): + return max(self.get_float_param("running_stitch_length_mm", 1.5), 0.01) + + @property + @param('fill_underlay', _('Underlay'), type='toggle', group=_('AutoFill Underlay'), default=True) + def fill_underlay(self): + return self.get_boolean_param("fill_underlay", default=True) + + @property + @param('fill_underlay_angle', + _('Fill angle'), + tooltip=_( + 'Default: fill angle + 90 deg. Insert comma-seperated list for multiple layers.'), + unit='deg', + group=_('AutoFill Underlay'), + type='float') + @cache + def fill_underlay_angle(self): + underlay_angles = self.get_param('fill_underlay_angle', None) + default_value = [self.angle + math.pi / 2.0] + if underlay_angles is not None: + underlay_angles = underlay_angles.strip().split(',') + try: + underlay_angles = [math.radians( + float(angle)) for angle in underlay_angles] + except (TypeError, ValueError): + return default_value + else: + underlay_angles = default_value + + return underlay_angles + + @property + @param('fill_underlay_row_spacing_mm', + _('Row spacing'), + tooltip=_('default: 3x fill row spacing'), + unit='mm', + group=_('AutoFill Underlay'), + type='float') + @cache + def fill_underlay_row_spacing(self): + return self.get_float_param("fill_underlay_row_spacing_mm") or self.row_spacing * 3 + + @property + @param('fill_underlay_max_stitch_length_mm', + _('Max stitch length'), + tooltip=_('default: equal to fill max stitch length'), + unit='mm', + group=_('AutoFill Underlay'), type='float') + @cache + def fill_underlay_max_stitch_length(self): + return self.get_float_param("fill_underlay_max_stitch_length_mm") or self.max_stitch_length + + @property + @param('fill_underlay_inset_mm', + _('Inset'), + tooltip=_( + 'Shrink the shape before doing underlay, to prevent underlay from showing around the outside of the fill.'), + unit='mm', + group=_('AutoFill Underlay'), + type='float', + default=0) + def fill_underlay_inset(self): + return self.get_float_param('fill_underlay_inset_mm', 0) + + @property + @param( + 'fill_underlay_skip_last', + _('Skip last stitch in each row'), + tooltip=_('The last stitch in each row is quite close to the first stitch in the next row. ' + 'Skipping it decreases stitch count and density.'), + group=_('AutoFill Underlay'), + type='boolean', + default=False) + def fill_underlay_skip_last(self): + return self.get_boolean_param("fill_underlay_skip_last", False) + + @property + @param('expand_mm', + _('Expand'), + tooltip=_( + 'Expand the shape before fill stitching, to compensate for gaps between shapes.'), + unit='mm', + type='float', + default=0, + sort_index=5, + select_items=[('fill_method', 0), ('fill_method', 2)]) + def expand(self): + return self.get_float_param('expand_mm', 0) + + @property + @param('underpath', + _('Underpath'), + tooltip=_('Travel inside the shape when moving from section to section. Underpath ' + 'stitches avoid traveling in the direction of the row angle so that they ' + 'are not visible. This gives them a jagged appearance.'), + type='boolean', + default=True, + select_items=[('fill_method', 0), ('fill_method', 2)], + sort_index=6) + def underpath(self): + return self.get_boolean_param('underpath', True) + + @property + @param( + 'underlay_underpath', + _('Underpath'), + tooltip=_('Travel inside the shape when moving from section to section. Underpath ' + 'stitches avoid traveling in the direction of the row angle so that they ' + 'are not visible. This gives them a jagged appearance.'), + group=_('AutoFill Underlay'), + type='boolean', + default=True) + def underlay_underpath(self): + return self.get_boolean_param('underlay_underpath', True) + + def shrink_or_grow_shape(self, amount, validate=False): + if amount: + shape = self.shape.buffer(amount) + # changing the size can empty the shape + # in this case we want to use the original shape rather than returning an error + if shape.is_empty and not validate: + return self.shape + if not isinstance(shape, shgeo.MultiPolygon): + shape = shgeo.MultiPolygon([shape]) + return shape + else: + return self.shape + + @property + def underlay_shape(self): + return self.shrink_or_grow_shape(-self.fill_underlay_inset) + + @property + def fill_shape(self): + return self.shrink_or_grow_shape(self.expand) + + def get_starting_point(self, last_patch): + # If there is a "fill_start" Command, then use that; otherwise pick + # the point closest to the end of the last patch. + + if self.get_command('fill_start'): + return self.get_command('fill_start').target_point + elif last_patch: + return last_patch.stitches[-1] + else: + return None + + def get_ending_point(self): + if self.get_command('fill_end'): + return self.get_command('fill_end').target_point + else: + return None + + def to_stitch_groups(self, last_patch): + # backwards compatibility: legacy_fill used to be inkstitch:auto_fill == False + if not self.auto_fill or self.fill_method == 3: + return self.do_legacy_fill() + else: + stitch_groups = [] + start = self.get_starting_point(last_patch) + end = self.get_ending_point() + + try: + if self.fill_underlay: + underlay_stitch_groups, start = self.do_underlay(start) + stitch_groups.extend(underlay_stitch_groups) + if self.fill_method == 0: + stitch_groups.extend(self.do_auto_fill(last_patch, start, end)) + if self.fill_method == 1: + stitch_groups.extend(self.do_tangential_fill(last_patch, start)) + elif self.fill_method == 2: + stitch_groups.extend(self.do_guided_fill(last_patch, start, end)) + except Exception: + self.fatal_fill_error() + + return stitch_groups + + def do_legacy_fill(self): + stitch_lists = legacy_fill(self.shape, + self.angle, + self.row_spacing, + self.end_row_spacing, + self.max_stitch_length, + self.flip, + self.staggers, + self.skip_last) + return [StitchGroup(stitches=stitch_list, color=self.color) for stitch_list in stitch_lists] + + def do_underlay(self, starting_point): + stitch_groups = [] + for i in range(len(self.fill_underlay_angle)): + underlay = StitchGroup( + color=self.color, + tags=("auto_fill", "auto_fill_underlay"), + stitches=auto_fill( + self.underlay_shape, + None, + self.fill_underlay_angle[i], + self.fill_underlay_row_spacing, + self.fill_underlay_row_spacing, + self.fill_underlay_max_stitch_length, + self.running_stitch_length, + self.staggers, + self.fill_underlay_skip_last, + starting_point, + underpath=self.underlay_underpath)) + stitch_groups.append(underlay) + + starting_point = underlay.stitches[-1] + return [stitch_groups, starting_point] + + def do_auto_fill(self, last_patch, starting_point, ending_point): + stitch_group = StitchGroup( + color=self.color, + tags=("auto_fill", "auto_fill_top"), + stitches=auto_fill( + self.fill_shape, + None, + self.angle, + self.row_spacing, + self.end_row_spacing, + self.max_stitch_length, + self.running_stitch_length, + self.staggers, + self.skip_last, + starting_point, + ending_point, + self.underpath)) + return [stitch_group] + + def do_tangential_fill(self, last_patch, starting_point): + stitch_groups = [] + polygons = list(self.fill_shape) + if not starting_point: + starting_point = (0, 0) + for poly in polygons: + connectedLine, connectedLineOrigin = StitchPattern.offset_poly( + poly, + -self.row_spacing, + self.join_style+1, + self.max_stitch_length, + self.interlaced, + self.tangential_strategy, + shgeo.Point(starting_point)) + path = [InkstitchPoint(*p) for p in connectedLine] + stitch_group = StitchGroup( + color=self.color, + tags=("auto_fill", "auto_fill_top"), + stitches=path) + stitch_groups.append(stitch_group) + + return stitch_groups + + def do_guided_fill(self, last_patch, starting_point, ending_point): + guide_line = self._get_guide_lines() + + # No guide line: fallback to normal autofill + if not guide_line: + return self.do_auto_fill(last_patch, starting_point, ending_point) + + stitch_group = StitchGroup( + color=self.color, + tags=("auto_fill", "auto_fill_top"), + stitches=auto_fill( + self.fill_shape, + guide_line.geoms[0], + self.angle, + self.row_spacing, + self.end_row_spacing, + self.max_stitch_length, + self.running_stitch_length, + 0, + self.skip_last, + starting_point, + ending_point, + self.underpath, + self.interlaced)) + return [stitch_group] + + @cache + def _get_guide_lines(self, multiple=False): + guide_lines = get_marker_elements(self.node, "guide-line", False, True) + # No or empty guide line + if not guide_lines or guide_lines['stroke'][0].is_empty: + return None + if multiple: + return guide_lines['stroke'] + else: + return guide_lines['stroke'][0] + + def fatal_fill_error(self): + if hasattr(sys, 'gettrace') and sys.gettrace(): + # if we're debugging, let the exception bubble up + raise + + # for an uncaught exception, give a little more info so that they can create a bug report + message = "" + message += _("Error during autofill! This means that there is a problem with Ink/Stitch.") + message += "\n\n" + # L10N this message is followed by a URL: https://github.com/inkstitch/inkstitch/issues/new + message += _("If you'd like to help us make Ink/Stitch better, please paste this whole message into a new issue at: ") + message += "https://github.com/inkstitch/inkstitch/issues/new\n\n" + message += version.get_inkstitch_version() + "\n\n" + message += traceback.format_exc() + + self.fatal(message) diff --git a/lib/elements/marker.py b/lib/elements/marker.py new file mode 100644 index 00000000..574ce91e --- /dev/null +++ b/lib/elements/marker.py @@ -0,0 +1,32 @@ +# 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 ..i18n import _ +from .element import EmbroideryElement +from .validation import ObjectTypeWarning + + +class MarkerWarning(ObjectTypeWarning): + name = _("Marker Element") + description = _("This element will not be embroidered. " + "It will be applied to objects in the same group. Objects in sub-groups will be ignored.") + steps_to_solve = [ + _("Turn back to normal embroidery element mode, remove the marker:"), + _('* Open the Fill and Stroke panel (Objects > Fill and Stroke)'), + _('* Go to the Stroke style tab'), + _('* Under "Markers" choose the first (empty) option in the first dropdown list.') + ] + + +class MarkerObject(EmbroideryElement): + + def validation_warnings(self): + repr_point = next(inkex.Path(self.parse_path()).end_points) + yield MarkerWarning(repr_point) + + def to_stitch_groups(self, last_patch): + return [] diff --git a/lib/elements/pattern.py b/lib/elements/pattern.py deleted file mode 100644 index 4b92d366..00000000 --- a/lib/elements/pattern.py +++ /dev/null @@ -1,33 +0,0 @@ -# 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 ..i18n import _ -from .element import EmbroideryElement -from .validation import ObjectTypeWarning - - -class PatternWarning(ObjectTypeWarning): - name = _("Pattern Element") - description = _("This element will not be embroidered. " - "It will appear as a pattern applied to objects in the same group as it. " - "Objects in sub-groups will be ignored.") - steps_to_solve = [ - _("To disable pattern mode, remove the pattern marker:"), - _('* Open the Fill and Stroke panel (Objects > Fill and Stroke)'), - _('* Go to the Stroke style tab'), - _('* Under "Markers" choose the first (empty) option in the first dropdown list.') - ] - - -class PatternObject(EmbroideryElement): - - def validation_warnings(self): - repr_point = next(inkex.Path(self.parse_path()).end_points) - yield PatternWarning(repr_point) - - def to_stitch_groups(self, last_patch): - return [] diff --git a/lib/elements/utils.py b/lib/elements/utils.py index 9b9b8f14..561188aa 100644 --- a/lib/elements/utils.py +++ b/lib/elements/utils.py @@ -7,12 +7,12 @@ from ..commands import is_command from ..marker import has_marker from ..svg.tags import (EMBROIDERABLE_TAGS, SVG_IMAGE_TAG, SVG_PATH_TAG, SVG_POLYLINE_TAG, SVG_TEXT_TAG) -from .auto_fill import AutoFill +from .fill_stitch import FillStitch from .clone import Clone, is_clone from .element import EmbroideryElement from .empty_d_object import EmptyDObject from .image import ImageObject -from .pattern import PatternObject +from .marker import MarkerObject from .polyline import Polyline from .satin_column import SatinColumn from .stroke import Stroke @@ -29,8 +29,8 @@ def node_to_elements(node): # noqa: C901 elif node.tag == SVG_PATH_TAG and not node.get('d', ''): return [EmptyDObject(node)] - elif has_marker(node, 'pattern'): - return [PatternObject(node)] + elif has_marker(node): + return [MarkerObject(node)] elif node.tag in EMBROIDERABLE_TAGS: element = EmbroideryElement(node) @@ -40,10 +40,7 @@ def node_to_elements(node): # noqa: C901 else: elements = [] if element.get_style("fill", "black") and not element.get_style('fill-opacity', 1) == "0": - # if element.get_boolean_param("auto_fill", True): - elements.append(AutoFill(node)) - # else: - # elements.append(Fill(node)) + elements.append(FillStitch(node)) if element.get_style("stroke"): if not is_command(element.node): elements.append(Stroke(node)) -- cgit v1.2.3 From b14e445daeafd12984cb40af289a415a0cb90e5d Mon Sep 17 00:00:00 2001 From: Andreas Date: Tue, 1 Feb 2022 19:47:19 +0100 Subject: small bug fix --- lib/elements/fill_stitch.py | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) (limited to 'lib/elements') diff --git a/lib/elements/fill_stitch.py b/lib/elements/fill_stitch.py index ee56abfc..2e67c517 100644 --- a/lib/elements/fill_stitch.py +++ b/lib/elements/fill_stitch.py @@ -43,14 +43,17 @@ class UnderlayInsetWarning(ValidationWarning): description = _("The underlay inset parameter for this fill object cannot be applied. " "Ink/Stitch will ignore it and will use the original size instead.") + class MissingGuideLineWarning(ValidationWarning): name = _("Missing Guideline") - description = _('This object is set to "Guided AutoFill", but has no guide line.') + description = _( + 'This object is set to "Guided AutoFill", but has no guide line.') steps_to_solve = [ _('* Create a stroke object'), _('* Select this object and run Extensions > Ink/Stitch > Edit > Selection to guide line') ] + class DisjointGuideLineWarning(ValidationWarning): name = _("Disjointed Guide Line") description = _("The guide line of this object isn't within the object borders. " @@ -59,13 +62,16 @@ class DisjointGuideLineWarning(ValidationWarning): _('* Move the guide line into the element') ] + class MultipleGuideLineWarning(ValidationWarning): name = _("Multiple Guide Lines") - description = _("This object has multiple guide lines, but only the first one will be used.") + description = _( + "This object has multiple guide lines, but only the first one will be used.") steps_to_solve = [ _("* Remove all guide lines, except for one.") ] + class UnconnectedError(ValidationError): name = _("Unconnected") description = _("Fill: This object is made up of unconnected shapes. This is not allowed because " @@ -78,7 +84,8 @@ class UnconnectedError(ValidationError): class InvalidShapeError(ValidationError): name = _("Border crosses itself") - description = _("Fill: Shape is not valid. This can happen if the border crosses over itself.") + description = _( + "Fill: Shape is not valid. This can happen if the border crosses over itself.") steps_to_solve = [ _('* Extensions > Ink/Stitch > Fill Tools > Break Apart Fill Objects') ] @@ -208,7 +215,8 @@ class FillStitch(EmbroideryElement): # ensure path length for i, path in enumerate(paths): if len(path) < 3: - paths[i] = [(path[0][0], path[0][1]), (path[0][0]+1.0, path[0][1]), (path[0][0], path[0][1]+1.0)] + paths[i] = [(path[0][0], path[0][1]), (path[0][0] + + 1.0, path[0][1]), (path[0][0], path[0][1]+1.0)] return paths @property @@ -218,7 +226,8 @@ class FillStitch(EmbroideryElement): # from the first. So let's at least make sure the "first" thing is the # biggest path. paths = self.paths - paths.sort(key=lambda point_list: shgeo.Polygon(point_list).area, reverse=True) + paths.sort(key=lambda point_list: shgeo.Polygon( + point_list).area, reverse=True) # Very small holes will cause a shape to be rendered as an outline only # they are too small to be rendered and only confuse the auto_fill algorithm. # So let's ignore them @@ -483,11 +492,14 @@ class FillStitch(EmbroideryElement): underlay_stitch_groups, start = self.do_underlay(start) stitch_groups.extend(underlay_stitch_groups) if self.fill_method == 0: - stitch_groups.extend(self.do_auto_fill(last_patch, start, end)) + stitch_groups.extend( + self.do_auto_fill(last_patch, start, end)) if self.fill_method == 1: - stitch_groups.extend(self.do_tangential_fill(last_patch, start)) + stitch_groups.extend( + self.do_tangential_fill(last_patch, start)) elif self.fill_method == 2: - stitch_groups.extend(self.do_guided_fill(last_patch, start, end)) + stitch_groups.extend( + self.do_guided_fill(last_patch, start, end)) except Exception: self.fatal_fill_error() @@ -599,8 +611,9 @@ class FillStitch(EmbroideryElement): def _get_guide_lines(self, multiple=False): guide_lines = get_marker_elements(self.node, "guide-line", False, True) # No or empty guide line - if not guide_lines or guide_lines['stroke'][0].is_empty: + if not guide_lines or not guide_lines['stroke']: return None + if multiple: return guide_lines['stroke'] else: -- cgit v1.2.3 From d514eac81937bb64815239dd3aa96e38d6556a32 Mon Sep 17 00:00:00 2001 From: Andreas Date: Wed, 2 Feb 2022 21:19:31 +0100 Subject: adjusting namings --- lib/elements/fill_stitch.py | 32 +++++++++++--------------------- 1 file changed, 11 insertions(+), 21 deletions(-) (limited to 'lib/elements') diff --git a/lib/elements/fill_stitch.py b/lib/elements/fill_stitch.py index 2e67c517..3256c1ea 100644 --- a/lib/elements/fill_stitch.py +++ b/lib/elements/fill_stitch.py @@ -15,14 +15,13 @@ from shapely.validation import explain_validity from ..i18n import _ from ..marker import get_marker_elements from ..stitch_plan import StitchGroup -from ..stitches import StitchPattern, auto_fill, legacy_fill +from ..stitches import tangential_fill_stitch_line_creator, auto_fill, legacy_fill from ..svg import PIXELS_PER_MM from ..svg.tags import INKSCAPE_LABEL from ..utils import Point as InkstitchPoint from ..utils import cache, version from .element import EmbroideryElement, param from .validation import ValidationError, ValidationWarning -from shapely.ops import nearest_points class SmallShapeWarning(ValidationWarning): @@ -46,8 +45,7 @@ class UnderlayInsetWarning(ValidationWarning): class MissingGuideLineWarning(ValidationWarning): name = _("Missing Guideline") - description = _( - 'This object is set to "Guided AutoFill", but has no guide line.') + description = _('This object is set to "Guided AutoFill", but has no guide line.') steps_to_solve = [ _('* Create a stroke object'), _('* Select this object and run Extensions > Ink/Stitch > Edit > Selection to guide line') @@ -65,8 +63,7 @@ class DisjointGuideLineWarning(ValidationWarning): class MultipleGuideLineWarning(ValidationWarning): name = _("Multiple Guide Lines") - description = _( - "This object has multiple guide lines, but only the first one will be used.") + description = _("This object has multiple guide lines, but only the first one will be used.") steps_to_solve = [ _("* Remove all guide lines, except for one.") ] @@ -84,8 +81,7 @@ class UnconnectedError(ValidationError): class InvalidShapeError(ValidationError): name = _("Border crosses itself") - description = _( - "Fill: Shape is not valid. This can happen if the border crosses over itself.") + description = _("Fill: Shape is not valid. This can happen if the border crosses over itself.") steps_to_solve = [ _('* Extensions > Ink/Stitch > Fill Tools > Break Apart Fill Objects') ] @@ -125,8 +121,7 @@ class FillStitch(EmbroideryElement): @property @param('angle', _('Angle of lines of stitches'), - tooltip=_( - 'The angle increases in a counter-clockwise direction. 0 is horizontal. Negative angles are allowed.'), + tooltip=_('The angle increases in a counter-clockwise direction. 0 is horizontal. Negative angles are allowed.'), unit='deg', type='float', sort_index=4, @@ -199,8 +194,7 @@ class FillStitch(EmbroideryElement): @property @param('staggers', _('Stagger rows this many times before repeating'), - tooltip=_( - 'Setting this dictates how many rows apart the stitches will be before they fall in the same column position.'), + tooltip=_('Setting this dictates how many rows apart the stitches will be before they fall in the same column position.'), type='int', sort_index=4, select_items=[('fill_method', 0), ('fill_method', 3)], @@ -317,8 +311,7 @@ class FillStitch(EmbroideryElement): @property @param('running_stitch_length_mm', _('Running stitch length (traversal between sections)'), - tooltip=_( - 'Length of stitches around the outline of the fill region used when moving from section to section.'), + tooltip=_('Length of stitches around the outline of the fill region used when moving from section to section.'), unit='mm', type='float', default=1.5, @@ -335,8 +328,7 @@ class FillStitch(EmbroideryElement): @property @param('fill_underlay_angle', _('Fill angle'), - tooltip=_( - 'Default: fill angle + 90 deg. Insert comma-seperated list for multiple layers.'), + tooltip=_('Default: fill angle + 90 deg. Insert comma-seperated list for multiple layers.'), unit='deg', group=_('AutoFill Underlay'), type='float') @@ -380,8 +372,7 @@ class FillStitch(EmbroideryElement): @property @param('fill_underlay_inset_mm', _('Inset'), - tooltip=_( - 'Shrink the shape before doing underlay, to prevent underlay from showing around the outside of the fill.'), + tooltip=_('Shrink the shape before doing underlay, to prevent underlay from showing around the outside of the fill.'), unit='mm', group=_('AutoFill Underlay'), type='float', @@ -404,8 +395,7 @@ class FillStitch(EmbroideryElement): @property @param('expand_mm', _('Expand'), - tooltip=_( - 'Expand the shape before fill stitching, to compensate for gaps between shapes.'), + tooltip=_('Expand the shape before fill stitching, to compensate for gaps between shapes.'), unit='mm', type='float', default=0, @@ -564,7 +554,7 @@ class FillStitch(EmbroideryElement): if not starting_point: starting_point = (0, 0) for poly in polygons: - connectedLine, connectedLineOrigin = StitchPattern.offset_poly( + connectedLine, _ = tangential_fill_stitch_line_creator.offset_poly( poly, -self.row_spacing, self.join_style+1, -- cgit v1.2.3 From 515ed3ea2fc8357482527d6e4a170db154baa205 Mon Sep 17 00:00:00 2001 From: Kaalleen Date: Fri, 18 Feb 2022 15:36:01 +0100 Subject: separate guided fill methods --- lib/elements/fill_stitch.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) (limited to 'lib/elements') diff --git a/lib/elements/fill_stitch.py b/lib/elements/fill_stitch.py index 3256c1ea..eaddcfe0 100644 --- a/lib/elements/fill_stitch.py +++ b/lib/elements/fill_stitch.py @@ -15,7 +15,7 @@ from shapely.validation import explain_validity from ..i18n import _ from ..marker import get_marker_elements from ..stitch_plan import StitchGroup -from ..stitches import tangential_fill_stitch_line_creator, auto_fill, legacy_fill +from ..stitches import tangential_fill_stitch_line_creator, auto_fill, legacy_fill, guided_fill from ..svg import PIXELS_PER_MM from ..svg.tags import INKSCAPE_LABEL from ..utils import Point as InkstitchPoint @@ -45,7 +45,7 @@ class UnderlayInsetWarning(ValidationWarning): class MissingGuideLineWarning(ValidationWarning): name = _("Missing Guideline") - description = _('This object is set to "Guided AutoFill", but has no guide line.') + description = _('This object is set to "Guided Fill", but has no guide line.') steps_to_solve = [ _('* Create a stroke object'), _('* Select this object and run Extensions > Ink/Stitch > Edit > Selection to guide line') @@ -97,7 +97,7 @@ class FillStitch(EmbroideryElement): @property @param('fill_method', _('Fill method'), type='dropdown', default=0, - options=[_("Auto Fill"), _("Tangential"), _("Guided Auto Fill"), _("Legacy Fill")], sort_index=2) + options=[_("Auto Fill"), _("Tangential"), _("Guided Fill"), _("Legacy Fill")], sort_index=2) def fill_method(self): return self.get_int_param('fill_method', 0) @@ -514,7 +514,6 @@ class FillStitch(EmbroideryElement): tags=("auto_fill", "auto_fill_underlay"), stitches=auto_fill( self.underlay_shape, - None, self.fill_underlay_angle[i], self.fill_underlay_row_spacing, self.fill_underlay_row_spacing, @@ -535,7 +534,6 @@ class FillStitch(EmbroideryElement): tags=("auto_fill", "auto_fill_top"), stitches=auto_fill( self.fill_shape, - None, self.angle, self.row_spacing, self.end_row_spacing, @@ -580,16 +578,14 @@ class FillStitch(EmbroideryElement): stitch_group = StitchGroup( color=self.color, - tags=("auto_fill", "auto_fill_top"), - stitches=auto_fill( + tags=("guided_fill", "auto_fill_top"), + stitches=guided_fill( self.fill_shape, guide_line.geoms[0], self.angle, self.row_spacing, - self.end_row_spacing, self.max_stitch_length, self.running_stitch_length, - 0, self.skip_last, starting_point, ending_point, -- cgit v1.2.3 From 6916a3371695205ca388daa37e3b9a0cc8d51de6 Mon Sep 17 00:00:00 2001 From: Andreas Date: Sun, 20 Mar 2022 17:15:39 +0100 Subject: bug fixing + introduction of min_stitch_distance parameter --- lib/elements/fill_stitch.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) (limited to 'lib/elements') diff --git a/lib/elements/fill_stitch.py b/lib/elements/fill_stitch.py index eaddcfe0..e0de0f22 100644 --- a/lib/elements/fill_stitch.py +++ b/lib/elements/fill_stitch.py @@ -191,6 +191,19 @@ class FillStitch(EmbroideryElement): def max_stitch_length(self): return max(self.get_float_param("max_stitch_length_mm", 3.0), 0.1 * PIXELS_PER_MM) + @property + @param('min_stitch_length_mm', + _('Minimum fill stitch length'), + tooltip=_( + 'The minimum length of a stitch in a row. Larger values might introduce deviations from the desired path. Shorter stitch may be used at the start or end of a row.'), + unit='mm', + sort_index=4, + select_items=[('fill_method', 1), ('fill_method', 2)], + type='float', + default=0.0) + def min_stitch_length(self): + return self.get_float_param("min_stitch_length_mm", 0.0) + @property @param('staggers', _('Stagger rows this many times before repeating'), @@ -557,6 +570,7 @@ class FillStitch(EmbroideryElement): -self.row_spacing, self.join_style+1, self.max_stitch_length, + min(self.min_stitch_length, self.max_stitch_length), self.interlaced, self.tangential_strategy, shgeo.Point(starting_point)) @@ -585,6 +599,7 @@ class FillStitch(EmbroideryElement): self.angle, self.row_spacing, self.max_stitch_length, + min(self.min_stitch_length,self.max_stitch_length), self.running_stitch_length, self.skip_last, starting_point, -- cgit v1.2.3 From 920063b324fd59798bc462c644bce8fc543f535b Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Wed, 6 Apr 2022 08:12:45 -0400 Subject: fix style --- lib/elements/fill_stitch.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'lib/elements') diff --git a/lib/elements/fill_stitch.py b/lib/elements/fill_stitch.py index e0de0f22..9233b6cf 100644 --- a/lib/elements/fill_stitch.py +++ b/lib/elements/fill_stitch.py @@ -195,7 +195,8 @@ class FillStitch(EmbroideryElement): @param('min_stitch_length_mm', _('Minimum fill stitch length'), tooltip=_( - 'The minimum length of a stitch in a row. Larger values might introduce deviations from the desired path. Shorter stitch may be used at the start or end of a row.'), + 'The minimum length of a stitches in a row. Larger values might introduce deviations from the desired path.' + 'Shorter stitch may be used at the start or end of a row.'), unit='mm', sort_index=4, select_items=[('fill_method', 1), ('fill_method', 2)], @@ -599,7 +600,7 @@ class FillStitch(EmbroideryElement): self.angle, self.row_spacing, self.max_stitch_length, - min(self.min_stitch_length,self.max_stitch_length), + min(self.min_stitch_length, self.max_stitch_length), self.running_stitch_length, self.skip_last, starting_point, -- cgit v1.2.3 From e2ede5e456d8037552ac9077f2cc34ccdfb52db2 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Thu, 28 Apr 2022 23:25:52 -0400 Subject: get rid of "closest point" strategy --- lib/elements/fill_stitch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib/elements') diff --git a/lib/elements/fill_stitch.py b/lib/elements/fill_stitch.py index 9233b6cf..3b87ea0c 100644 --- a/lib/elements/fill_stitch.py +++ b/lib/elements/fill_stitch.py @@ -103,7 +103,7 @@ class FillStitch(EmbroideryElement): @property @param('tangential_strategy', _('Tangential strategy'), type='dropdown', default=1, - options=[_("Closest point"), _("Inner to Outer"), _("Single spiral")], select_items=[('fill_method', 1)], sort_index=2) + options=[_("Inner to Outer"), _("Single spiral")], select_items=[('fill_method', 1)], sort_index=2) def tangential_strategy(self): return self.get_int_param('tangential_strategy', 1) -- cgit v1.2.3 From 60fb7d0a9efa43d3b58867927ecede6cfdc5ab21 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Mon, 2 May 2022 14:47:43 -0400 Subject: fix more shapely deprecations --- lib/elements/fill_stitch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib/elements') diff --git a/lib/elements/fill_stitch.py b/lib/elements/fill_stitch.py index 3b87ea0c..4157d3fb 100644 --- a/lib/elements/fill_stitch.py +++ b/lib/elements/fill_stitch.py @@ -562,7 +562,7 @@ class FillStitch(EmbroideryElement): def do_tangential_fill(self, last_patch, starting_point): stitch_groups = [] - polygons = list(self.fill_shape) + polygons = self.fill_shape.geoms if not starting_point: starting_point = (0, 0) for poly in polygons: -- cgit v1.2.3 From 76ab3197317f258ede1bd98195535f33b856b3fd Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Mon, 2 May 2022 15:00:52 -0400 Subject: add avoid_self_Crossing option --- lib/elements/fill_stitch.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) (limited to 'lib/elements') diff --git a/lib/elements/fill_stitch.py b/lib/elements/fill_stitch.py index 4157d3fb..bc022ab3 100644 --- a/lib/elements/fill_stitch.py +++ b/lib/elements/fill_stitch.py @@ -118,6 +118,11 @@ class FillStitch(EmbroideryElement): def interlaced(self): return self.get_boolean_param('interlaced', True) + @property + @param('avoid_self_crossing', _('Avoid self-crossing'), type='boolean', default=False, select_items=[('fill_method', 1)], sort_index=2) + def avoid_self_crossing(self): + return self.get_boolean_param('avoid_self_crossing', False) + @property @param('angle', _('Angle of lines of stitches'), @@ -569,12 +574,14 @@ class FillStitch(EmbroideryElement): connectedLine, _ = tangential_fill_stitch_line_creator.offset_poly( poly, -self.row_spacing, - self.join_style+1, + self.join_style + 1, self.max_stitch_length, min(self.min_stitch_length, self.max_stitch_length), self.interlaced, self.tangential_strategy, - shgeo.Point(starting_point)) + shgeo.Point(starting_point), + self.avoid_self_crossing + ) path = [InkstitchPoint(*p) for p in connectedLine] stitch_group = StitchGroup( color=self.color, -- cgit v1.2.3 From 68ee0eea8733d613543a28627bd21a4481da8b46 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Mon, 2 May 2022 23:48:46 -0400 Subject: add clockwise option --- lib/elements/fill_stitch.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) (limited to 'lib/elements') diff --git a/lib/elements/fill_stitch.py b/lib/elements/fill_stitch.py index bc022ab3..2f8687a1 100644 --- a/lib/elements/fill_stitch.py +++ b/lib/elements/fill_stitch.py @@ -123,6 +123,11 @@ class FillStitch(EmbroideryElement): def avoid_self_crossing(self): return self.get_boolean_param('avoid_self_crossing', False) + @property + @param('clockwise', _('Clockwise'), type='boolean', default=True, select_items=[('fill_method', 1), ('fill_method', 2)], sort_index=2) + def clockwise(self): + return self.get_boolean_param('clockwise', True) + @property @param('angle', _('Angle of lines of stitches'), @@ -580,7 +585,8 @@ class FillStitch(EmbroideryElement): self.interlaced, self.tangential_strategy, shgeo.Point(starting_point), - self.avoid_self_crossing + self.avoid_self_crossing, + self.clockwise ) path = [InkstitchPoint(*p) for p in connectedLine] stitch_group = StitchGroup( -- cgit v1.2.3 From 5a69fa3e9c582a3bc21660342cea35837ae1eb9a Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Tue, 3 May 2022 14:34:21 -0400 Subject: add double (fermat) spiral --- lib/elements/fill_stitch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib/elements') diff --git a/lib/elements/fill_stitch.py b/lib/elements/fill_stitch.py index 2f8687a1..70d4f356 100644 --- a/lib/elements/fill_stitch.py +++ b/lib/elements/fill_stitch.py @@ -103,7 +103,7 @@ class FillStitch(EmbroideryElement): @property @param('tangential_strategy', _('Tangential strategy'), type='dropdown', default=1, - options=[_("Inner to Outer"), _("Single spiral")], select_items=[('fill_method', 1)], sort_index=2) + options=[_("Inner to Outer"), _("Single spiral"), _("Double spiral")], select_items=[('fill_method', 1)], sort_index=2) def tangential_strategy(self): return self.get_int_param('tangential_strategy', 1) -- cgit v1.2.3 From aeeaf72338e2d7645309725be641d552a3c56190 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Tue, 3 May 2022 16:58:55 -0400 Subject: wip --- lib/elements/fill_stitch.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) (limited to 'lib/elements') diff --git a/lib/elements/fill_stitch.py b/lib/elements/fill_stitch.py index 70d4f356..5e795f45 100644 --- a/lib/elements/fill_stitch.py +++ b/lib/elements/fill_stitch.py @@ -576,19 +576,17 @@ class FillStitch(EmbroideryElement): if not starting_point: starting_point = (0, 0) for poly in polygons: - connectedLine, _ = tangential_fill_stitch_line_creator.offset_poly( + connected_line = tangential_fill_stitch_line_creator.tangential_fill( poly, - -self.row_spacing, - self.join_style + 1, - self.max_stitch_length, - min(self.min_stitch_length, self.max_stitch_length), - self.interlaced, self.tangential_strategy, + self.row_spacing, + self.max_stitch_length, + self.join_style + 1, + self.clockwise, shgeo.Point(starting_point), self.avoid_self_crossing, - self.clockwise ) - path = [InkstitchPoint(*p) for p in connectedLine] + path = [InkstitchPoint(*p) for p in connected_line] stitch_group = StitchGroup( color=self.color, tags=("auto_fill", "auto_fill_top"), -- cgit v1.2.3 From 330c6be78786b85ed2528cf2788e529cfda714fd Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Tue, 3 May 2022 21:08:48 -0400 Subject: refactor, tidy, and C901 fixes --- lib/elements/fill_stitch.py | 51 +++++++++++++++++++++++++++------------------ 1 file changed, 31 insertions(+), 20 deletions(-) (limited to 'lib/elements') diff --git a/lib/elements/fill_stitch.py b/lib/elements/fill_stitch.py index 5e795f45..d7b859b5 100644 --- a/lib/elements/fill_stitch.py +++ b/lib/elements/fill_stitch.py @@ -15,10 +15,9 @@ from shapely.validation import explain_validity from ..i18n import _ from ..marker import get_marker_elements from ..stitch_plan import StitchGroup -from ..stitches import tangential_fill_stitch_line_creator, auto_fill, legacy_fill, guided_fill +from ..stitches import tangential_fill, auto_fill, legacy_fill, guided_fill from ..svg import PIXELS_PER_MM from ..svg.tags import INKSCAPE_LABEL -from ..utils import Point as InkstitchPoint from ..utils import cache, version from .element import EmbroideryElement, param from .validation import ValidationError, ValidationWarning @@ -571,26 +570,40 @@ class FillStitch(EmbroideryElement): return [stitch_group] def do_tangential_fill(self, last_patch, starting_point): - stitch_groups = [] - polygons = self.fill_shape.geoms if not starting_point: starting_point = (0, 0) - for poly in polygons: - connected_line = tangential_fill_stitch_line_creator.tangential_fill( - poly, - self.tangential_strategy, - self.row_spacing, - self.max_stitch_length, - self.join_style + 1, - self.clockwise, - shgeo.Point(starting_point), - self.avoid_self_crossing, - ) - path = [InkstitchPoint(*p) for p in connected_line] + starting_point = shgeo.Point(starting_point) + + stitch_groups = [] + for polygon in self.fill_shape.geoms: + tree = tangential_fill.offset_polygon(polygon, self.row_spacing, self.join_style + 1, self.clockwise) + + stitches = [] + if self.tangential_strategy == 0: + stitches = tangential_fill.inner_to_outer( + tree, + self.row_spacing, + self.max_stitch_length, + starting_point, + self.avoid_self_crossing + ) + elif self.tangential_strategy == 1: + stitches = tangential_fill.single_spiral( + tree, + self.max_stitch_length, + starting_point + ) + elif self.tangential_strategy == 2: + stitches = tangential_fill.double_spiral( + tree, + self.max_stitch_length, + starting_point + ) + stitch_group = StitchGroup( color=self.color, tags=("auto_fill", "auto_fill_top"), - stitches=path) + stitches=stitches) stitch_groups.append(stitch_group) return stitch_groups @@ -611,13 +624,11 @@ class FillStitch(EmbroideryElement): self.angle, self.row_spacing, self.max_stitch_length, - min(self.min_stitch_length, self.max_stitch_length), self.running_stitch_length, self.skip_last, starting_point, ending_point, - self.underpath, - self.interlaced)) + self.underpath)) return [stitch_group] @cache -- cgit v1.2.3 From 48d0a0250e2787a3351137172924d1c4d277f002 Mon Sep 17 00:00:00 2001 From: Kaalleen Date: Wed, 4 May 2022 18:27:12 +0200 Subject: undo build changes for depq, update clone --- lib/elements/clone.py | 2 +- lib/elements/utils.py | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) (limited to 'lib/elements') diff --git a/lib/elements/clone.py b/lib/elements/clone.py index 3f133471..303c1c2f 100644 --- a/lib/elements/clone.py +++ b/lib/elements/clone.py @@ -70,7 +70,7 @@ class Clone(EmbroideryElement): def clone_to_element(self, node): from .utils import node_to_elements - return node_to_elements(node) + return node_to_elements(node, True) def to_stitch_groups(self, last_patch=None): patches = [] diff --git a/lib/elements/utils.py b/lib/elements/utils.py index 561188aa..dafde759 100644 --- a/lib/elements/utils.py +++ b/lib/elements/utils.py @@ -19,11 +19,12 @@ from .stroke import Stroke from .text import TextObject -def node_to_elements(node): # noqa: C901 +def node_to_elements(node, clone_to_element=False): # noqa: C901 if node.tag == SVG_POLYLINE_TAG: return [Polyline(node)] - elif is_clone(node): + elif is_clone(node) and not clone_to_element: + # clone_to_element: get an actual embroiderable element once a clone has been defined as a clone return [Clone(node)] elif node.tag == SVG_PATH_TAG and not node.get('d', ''): @@ -32,7 +33,7 @@ def node_to_elements(node): # noqa: C901 elif has_marker(node): return [MarkerObject(node)] - elif node.tag in EMBROIDERABLE_TAGS: + elif node.tag in EMBROIDERABLE_TAGS or is_clone(node): element = EmbroideryElement(node) if element.get_boolean_param("satin_column") and element.get_style("stroke"): -- cgit v1.2.3 From e65aaebbcab1ca6fbcf99d9f3665af423e02c2f5 Mon Sep 17 00:00:00 2001 From: Kaalleen Date: Wed, 4 May 2022 20:04:39 +0200 Subject: rebase corrections --- lib/elements/clone.py | 6 ------ 1 file changed, 6 deletions(-) (limited to 'lib/elements') diff --git a/lib/elements/clone.py b/lib/elements/clone.py index 303c1c2f..d9185012 100644 --- a/lib/elements/clone.py +++ b/lib/elements/clone.py @@ -5,13 +5,7 @@ from math import atan, degrees -<<<<<<< HEAD -from ..commands import is_command, is_command_symbol -======= -import inkex - from ..commands import is_command_symbol ->>>>>>> c69b6f5a (* autofill to fillstitch) from ..i18n import _ from ..svg.path import get_node_transform from ..svg.svg import find_elements -- cgit v1.2.3 From a275d49a24dc91b734c6dbee1e29157bfd0d59d5 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Thu, 5 May 2022 22:53:31 -0400 Subject: tangential->contour, fix legacy, remove unused params --- lib/elements/element.py | 4 +-- lib/elements/fill_stitch.py | 76 ++++++++++++++++----------------------------- 2 files changed, 29 insertions(+), 51 deletions(-) (limited to 'lib/elements') diff --git a/lib/elements/element.py b/lib/elements/element.py index ee4eadbb..3f5c6f4a 100644 --- a/lib/elements/element.py +++ b/lib/elements/element.py @@ -208,7 +208,7 @@ class EmbroideryElement(object): # L10N options to allow lock stitch before and after objects options=[_("Both"), _("Before"), _("After"), _("Neither")], default=0, - sort_index=4) + sort_index=10) @cache def ties(self): return self.get_int_param("ties", 0) @@ -220,7 +220,7 @@ class EmbroideryElement(object): 'even if the distance to the next object is shorter than defined by the collapse length value in the Ink/Stitch preferences.'), type='boolean', default=False, - sort_index=5) + sort_index=10) @cache def force_lock_stitches(self): return self.get_boolean_param('force_lock_stitches', False) diff --git a/lib/elements/fill_stitch.py b/lib/elements/fill_stitch.py index d7b859b5..c1bba7b8 100644 --- a/lib/elements/fill_stitch.py +++ b/lib/elements/fill_stitch.py @@ -15,7 +15,7 @@ from shapely.validation import explain_validity from ..i18n import _ from ..marker import get_marker_elements from ..stitch_plan import StitchGroup -from ..stitches import tangential_fill, auto_fill, legacy_fill, guided_fill +from ..stitches import contour_fill, auto_fill, legacy_fill, guided_fill from ..svg import PIXELS_PER_MM from ..svg.tags import INKSCAPE_LABEL from ..utils import cache, version @@ -96,34 +96,29 @@ class FillStitch(EmbroideryElement): @property @param('fill_method', _('Fill method'), type='dropdown', default=0, - options=[_("Auto Fill"), _("Tangential"), _("Guided Fill"), _("Legacy Fill")], sort_index=2) + options=[_("Auto Fill"), _("Contour Fill"), _("Guided Fill"), _("Legacy Fill")], sort_index=2) def fill_method(self): return self.get_int_param('fill_method', 0) @property - @param('tangential_strategy', _('Tangential strategy'), type='dropdown', default=1, - options=[_("Inner to Outer"), _("Single spiral"), _("Double spiral")], select_items=[('fill_method', 1)], sort_index=2) - def tangential_strategy(self): - return self.get_int_param('tangential_strategy', 1) + @param('contour_strategy', _('Contour Fill Strategy'), type='dropdown', default=1, + options=[_("Inner to Outer"), _("Single spiral"), _("Double spiral")], select_items=[('fill_method', 1)], sort_index=3) + def contour_strategy(self): + return self.get_int_param('contour_strategy', 1) @property @param('join_style', _('Join Style'), type='dropdown', default=0, - options=[_("Round"), _("Mitered"), _("Beveled")], select_items=[('fill_method', 1)], sort_index=2) + options=[_("Round"), _("Mitered"), _("Beveled")], select_items=[('fill_method', 1)], sort_index=4) def join_style(self): return self.get_int_param('join_style', 0) @property - @param('interlaced', _('Interlaced'), type='boolean', default=True, select_items=[('fill_method', 1), ('fill_method', 2)], sort_index=2) - def interlaced(self): - return self.get_boolean_param('interlaced', True) - - @property - @param('avoid_self_crossing', _('Avoid self-crossing'), type='boolean', default=False, select_items=[('fill_method', 1)], sort_index=2) + @param('avoid_self_crossing', _('Avoid self-crossing'), type='boolean', default=False, select_items=[('fill_method', 1)], sort_index=5) def avoid_self_crossing(self): return self.get_boolean_param('avoid_self_crossing', False) @property - @param('clockwise', _('Clockwise'), type='boolean', default=True, select_items=[('fill_method', 1), ('fill_method', 2)], sort_index=2) + @param('clockwise', _('Clockwise'), type='boolean', default=True, select_items=[('fill_method', 1)], sort_index=5) def clockwise(self): return self.get_boolean_param('clockwise', True) @@ -133,7 +128,7 @@ class FillStitch(EmbroideryElement): tooltip=_('The angle increases in a counter-clockwise direction. 0 is horizontal. Negative angles are allowed.'), unit='deg', type='float', - sort_index=4, + sort_index=6, select_items=[('fill_method', 0), ('fill_method', 3)], default=0) @cache @@ -152,7 +147,7 @@ class FillStitch(EmbroideryElement): tooltip=_('The last stitch in each row is quite close to the first stitch in the next row. ' 'Skipping it decreases stitch count and density.'), type='boolean', - sort_index=4, + sort_index=6, select_items=[('fill_method', 0), ('fill_method', 2), ('fill_method', 3)], default=False) @@ -166,7 +161,7 @@ class FillStitch(EmbroideryElement): tooltip=_('The flip option can help you with routing your stitch path. ' 'When you enable flip, stitching goes from right-to-left instead of left-to-right.'), type='boolean', - sort_index=4, + sort_index=7, select_items=[('fill_method', 0), ('fill_method', 2), ('fill_method', 3)], default=False) @@ -178,7 +173,7 @@ class FillStitch(EmbroideryElement): _('Spacing between rows'), tooltip=_('Distance between rows of stitches.'), unit='mm', - sort_index=4, + sort_index=6, type='float', default=0.25) def row_spacing(self): @@ -194,32 +189,18 @@ class FillStitch(EmbroideryElement): tooltip=_( 'The length of each stitch in a row. Shorter stitch may be used at the start or end of a row.'), unit='mm', - sort_index=4, + sort_index=6, type='float', default=3.0) def max_stitch_length(self): return max(self.get_float_param("max_stitch_length_mm", 3.0), 0.1 * PIXELS_PER_MM) - @property - @param('min_stitch_length_mm', - _('Minimum fill stitch length'), - tooltip=_( - 'The minimum length of a stitches in a row. Larger values might introduce deviations from the desired path.' - 'Shorter stitch may be used at the start or end of a row.'), - unit='mm', - sort_index=4, - select_items=[('fill_method', 1), ('fill_method', 2)], - type='float', - default=0.0) - def min_stitch_length(self): - return self.get_float_param("min_stitch_length_mm", 0.0) - @property @param('staggers', _('Stagger rows this many times before repeating'), tooltip=_('Setting this dictates how many rows apart the stitches will be before they fall in the same column position.'), type='int', - sort_index=4, + sort_index=6, select_items=[('fill_method', 0), ('fill_method', 3)], default=4) def staggers(self): @@ -339,7 +320,7 @@ class FillStitch(EmbroideryElement): type='float', default=1.5, select_items=[('fill_method', 0), ('fill_method', 2)], - sort_index=4) + sort_index=6) def running_stitch_length(self): return max(self.get_float_param("running_stitch_length_mm", 1.5), 0.01) @@ -505,14 +486,11 @@ class FillStitch(EmbroideryElement): underlay_stitch_groups, start = self.do_underlay(start) stitch_groups.extend(underlay_stitch_groups) if self.fill_method == 0: - stitch_groups.extend( - self.do_auto_fill(last_patch, start, end)) + stitch_groups.extend(self.do_auto_fill(last_patch, start, end)) if self.fill_method == 1: - stitch_groups.extend( - self.do_tangential_fill(last_patch, start)) + stitch_groups.extend(self.do_contour_fill(last_patch, start)) elif self.fill_method == 2: - stitch_groups.extend( - self.do_guided_fill(last_patch, start, end)) + stitch_groups.extend(self.do_guided_fill(last_patch, start, end)) except Exception: self.fatal_fill_error() @@ -569,32 +547,32 @@ class FillStitch(EmbroideryElement): self.underpath)) return [stitch_group] - def do_tangential_fill(self, last_patch, starting_point): + def do_contour_fill(self, last_patch, starting_point): if not starting_point: starting_point = (0, 0) starting_point = shgeo.Point(starting_point) stitch_groups = [] for polygon in self.fill_shape.geoms: - tree = tangential_fill.offset_polygon(polygon, self.row_spacing, self.join_style + 1, self.clockwise) + tree = contour_fill.offset_polygon(polygon, self.row_spacing, self.join_style + 1, self.clockwise) stitches = [] - if self.tangential_strategy == 0: - stitches = tangential_fill.inner_to_outer( + if self.contour_strategy == 0: + stitches = contour_fill.inner_to_outer( tree, self.row_spacing, self.max_stitch_length, starting_point, self.avoid_self_crossing ) - elif self.tangential_strategy == 1: - stitches = tangential_fill.single_spiral( + elif self.contour_strategy == 1: + stitches = contour_fill.single_spiral( tree, self.max_stitch_length, starting_point ) - elif self.tangential_strategy == 2: - stitches = tangential_fill.double_spiral( + elif self.contour_strategy == 2: + stitches = contour_fill.double_spiral( tree, self.max_stitch_length, starting_point -- cgit v1.2.3 From b30fce85dbdb4097bb9e01c3d68a77e0c50dd80a Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Sat, 7 May 2022 16:20:15 -0400 Subject: undo aggressive line wrapping --- lib/elements/element.py | 21 +++++++-------------- lib/elements/fill_stitch.py | 3 +-- 2 files changed, 8 insertions(+), 16 deletions(-) (limited to 'lib/elements') diff --git a/lib/elements/element.py b/lib/elements/element.py index 3f5c6f4a..3648760b 100644 --- a/lib/elements/element.py +++ b/lib/elements/element.py @@ -166,8 +166,7 @@ class EmbroideryElement(object): # Of course, transforms may also involve rotation, skewing, and translation. # All except translation can affect how wide the stroke appears on the screen. - node_transform = inkex.transforms.Transform( - get_node_transform(self.node)) + node_transform = inkex.transforms.Transform(get_node_transform(self.node)) # First, figure out the translation component of the transform. Using a zero # vector completely cancels out the rotation, scale, and skew components. @@ -201,8 +200,7 @@ class EmbroideryElement(object): @property @param('ties', _('Allow lock stitches'), - tooltip=_( - 'Tie thread at the beginning and/or end of this object. Manual stitch will not add lock stitches.'), + tooltip=_('Tie thread at the beginning and/or end of this object. Manual stitch will not add lock stitches.'), type='dropdown', # Ties: 0 = Both | 1 = Before | 2 = After | 3 = Neither # L10N options to allow lock stitch before and after objects @@ -260,8 +258,7 @@ class EmbroideryElement(object): d = self.node.get("d", "") if not d: - self.fatal(_("Object %(id)s has an empty 'd' attribute. Please delete this object from your document.") % dict( - id=self.node.get("id"))) + self.fatal(_("Object %(id)s has an empty 'd' attribute. Please delete this object from your document.") % dict(id=self.node.get("id"))) return inkex.paths.Path(d).to_superpath() @@ -276,8 +273,7 @@ class EmbroideryElement(object): @property def shape(self): - raise NotImplementedError( - "INTERNAL ERROR: %s must implement shape()", self.__class__) + raise NotImplementedError("INTERNAL ERROR: %s must implement shape()", self.__class__) @property @cache @@ -327,8 +323,7 @@ class EmbroideryElement(object): return self.get_boolean_param('stop_after', False) def to_stitch_groups(self, last_patch): - raise NotImplementedError( - "%s must implement to_stitch_groups()" % self.__class__.__name__) + raise NotImplementedError("%s must implement to_stitch_groups()" % self.__class__.__name__) def embroider(self, last_patch): self.validate() @@ -341,10 +336,8 @@ class EmbroideryElement(object): patch.force_lock_stitches = self.force_lock_stitches if patches: - patches[-1].trim_after = self.has_command( - "trim") or self.trim_after - patches[-1].stop_after = self.has_command( - "stop") or self.stop_after + patches[-1].trim_after = self.has_command("trim") or self.trim_after + patches[-1].stop_after = self.has_command("stop") or self.stop_after return patches diff --git a/lib/elements/fill_stitch.py b/lib/elements/fill_stitch.py index c1bba7b8..58629085 100644 --- a/lib/elements/fill_stitch.py +++ b/lib/elements/fill_stitch.py @@ -213,8 +213,7 @@ class FillStitch(EmbroideryElement): # ensure path length for i, path in enumerate(paths): if len(path) < 3: - paths[i] = [(path[0][0], path[0][1]), (path[0][0] + - 1.0, path[0][1]), (path[0][0], path[0][1]+1.0)] + paths[i] = [(path[0][0], path[0][1]), (path[0][0] + 1.0, path[0][1]), (path[0][0], path[0][1] + 1.0)] return paths @property -- cgit v1.2.3 From d0fc0e13261f5f37247570793dea726786df5456 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Sat, 7 May 2022 23:14:55 -0400 Subject: default to 'inner to outer' --- lib/elements/fill_stitch.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'lib/elements') diff --git a/lib/elements/fill_stitch.py b/lib/elements/fill_stitch.py index 58629085..c9d609b4 100644 --- a/lib/elements/fill_stitch.py +++ b/lib/elements/fill_stitch.py @@ -101,10 +101,10 @@ class FillStitch(EmbroideryElement): return self.get_int_param('fill_method', 0) @property - @param('contour_strategy', _('Contour Fill Strategy'), type='dropdown', default=1, + @param('contour_strategy', _('Contour Fill Strategy'), type='dropdown', default=0, options=[_("Inner to Outer"), _("Single spiral"), _("Double spiral")], select_items=[('fill_method', 1)], sort_index=3) def contour_strategy(self): - return self.get_int_param('contour_strategy', 1) + return self.get_int_param('contour_strategy', 0) @property @param('join_style', _('Join Style'), type='dropdown', default=0, -- cgit v1.2.3 From 87f328ec851aa418772d6b459cf6797248deb98f Mon Sep 17 00:00:00 2001 From: Kaalleen Date: Tue, 17 May 2022 17:33:10 +0200 Subject: * flip option only for legacyfill * rename autofill underlay to fill underlay * remove unused dependency --- lib/elements/fill_stitch.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) (limited to 'lib/elements') diff --git a/lib/elements/fill_stitch.py b/lib/elements/fill_stitch.py index c9d609b4..f1a75e2f 100644 --- a/lib/elements/fill_stitch.py +++ b/lib/elements/fill_stitch.py @@ -162,8 +162,7 @@ class FillStitch(EmbroideryElement): 'When you enable flip, stitching goes from right-to-left instead of left-to-right.'), type='boolean', sort_index=7, - select_items=[('fill_method', 0), ('fill_method', 2), - ('fill_method', 3)], + select_items=[('fill_method', 3)], default=False) def flip(self): return self.get_boolean_param("flip", False) @@ -324,7 +323,7 @@ class FillStitch(EmbroideryElement): return max(self.get_float_param("running_stitch_length_mm", 1.5), 0.01) @property - @param('fill_underlay', _('Underlay'), type='toggle', group=_('AutoFill Underlay'), default=True) + @param('fill_underlay', _('Underlay'), type='toggle', group=_('Fill Underlay'), default=True) def fill_underlay(self): return self.get_boolean_param("fill_underlay", default=True) @@ -333,7 +332,7 @@ class FillStitch(EmbroideryElement): _('Fill angle'), tooltip=_('Default: fill angle + 90 deg. Insert comma-seperated list for multiple layers.'), unit='deg', - group=_('AutoFill Underlay'), + group=_('Fill Underlay'), type='float') @cache def fill_underlay_angle(self): @@ -356,7 +355,7 @@ class FillStitch(EmbroideryElement): _('Row spacing'), tooltip=_('default: 3x fill row spacing'), unit='mm', - group=_('AutoFill Underlay'), + group=_('Fill Underlay'), type='float') @cache def fill_underlay_row_spacing(self): @@ -367,7 +366,7 @@ class FillStitch(EmbroideryElement): _('Max stitch length'), tooltip=_('default: equal to fill max stitch length'), unit='mm', - group=_('AutoFill Underlay'), type='float') + group=_('Fill Underlay'), type='float') @cache def fill_underlay_max_stitch_length(self): return self.get_float_param("fill_underlay_max_stitch_length_mm") or self.max_stitch_length @@ -377,7 +376,7 @@ class FillStitch(EmbroideryElement): _('Inset'), tooltip=_('Shrink the shape before doing underlay, to prevent underlay from showing around the outside of the fill.'), unit='mm', - group=_('AutoFill Underlay'), + group=_('Fill Underlay'), type='float', default=0) def fill_underlay_inset(self): @@ -389,7 +388,7 @@ class FillStitch(EmbroideryElement): _('Skip last stitch in each row'), tooltip=_('The last stitch in each row is quite close to the first stitch in the next row. ' 'Skipping it decreases stitch count and density.'), - group=_('AutoFill Underlay'), + group=_('Fill Underlay'), type='boolean', default=False) def fill_underlay_skip_last(self): @@ -427,7 +426,7 @@ class FillStitch(EmbroideryElement): tooltip=_('Travel inside the shape when moving from section to section. Underpath ' 'stitches avoid traveling in the direction of the row angle so that they ' 'are not visible. This gives them a jagged appearance.'), - group=_('AutoFill Underlay'), + group=_('Fill Underlay'), type='boolean', default=True) def underlay_underpath(self): -- cgit v1.2.3 From 47123198760f8740acda0799d3b22f14b3f69550 Mon Sep 17 00:00:00 2001 From: Kaalleen Date: Thu, 19 May 2022 20:03:37 +0200 Subject: avoid simple satin division by zero error --- lib/elements/stroke.py | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'lib/elements') diff --git a/lib/elements/stroke.py b/lib/elements/stroke.py index 307c78b8..c9c0f795 100644 --- a/lib/elements/stroke.py +++ b/lib/elements/stroke.py @@ -167,6 +167,10 @@ class Stroke(EmbroideryElement): for i in range(len(patch) - 1): start = patch.stitches[i] end = patch.stitches[i + 1] + # sometimes the stitch results into zero length which cause a division by zero error + # ignoring this leads to a slightly bad result, but that is better than no output + if (end - start).length() == 0: + continue segment_direction = (end - start).unit() zigzag_direction = segment_direction.rotate_left() -- cgit v1.2.3