diff options
Diffstat (limited to 'lib/elements/fill.py')
| -rw-r--r-- | lib/elements/fill.py | 205 |
1 files changed, 0 insertions, 205 deletions
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] |
