diff options
| author | Andreas <v.andreas.1@web.de> | 2021-10-21 16:24:40 +0200 |
|---|---|---|
| committer | Kaalleen <reni@allenka.de> | 2022-05-04 18:55:40 +0200 |
| commit | 0fcf8bb97ced8df552cd0283b4ea009b6ca42623 (patch) | |
| tree | 9fbb5053db31a15ec140ed95f7b2634e987fe12d /lib/elements/auto_fill.py | |
| parent | ea66eb8685524881ad34d412a8bbaeb482e5a9cf (diff) | |
added tangential and guided fill
Diffstat (limited to 'lib/elements/auto_fill.py')
| -rw-r--r-- | lib/elements/auto_fill.py | 281 |
1 files changed, 249 insertions, 32 deletions
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 |
