import math from shapely import geometry as shgeo from shapely.validation import explain_validity from ..i18n import _ from ..stitches import legacy_fill from ..svg import PIXELS_PER_MM from ..utils import cache from .element import param, EmbroideryElement, Patch 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 self.get_int_param("staggers", 4) @property @cache def paths(self): return self.flatten(self.parse_path()) @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) polygon = shgeo.MultiPolygon([(paths[0], paths[1:])]) if not polygon.is_valid: why = explain_validity(polygon) # I Wish this weren't so brittle... if "Hole lies outside shell" in why: self.fatal(_("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.")) else: self.fatal(_("shape is not valid. This can happen if the border crosses over itself.")) return polygon def to_patches(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 [Patch(stitches=stitch_list, color=self.color) for stitch_list in stitch_lists]