diff options
| author | Kaalleen <36401965+kaalleen@users.noreply.github.com> | 2023-04-24 22:52:31 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-04-24 22:52:31 +0200 |
| commit | e5ccb10eef83b88b722e94dc39f8bcc7692a3ce1 (patch) | |
| tree | ba17b2204c85532549b288d3f8338e7422162d69 /lib | |
| parent | 9b726f6436c87860cc3aef1bb24c61fb9e4e06cd (diff) | |
Add bean stitch and repeat options to meander fill (#2232)
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/elements/fill_stitch.py | 326 | ||||
| -rw-r--r-- | lib/stitches/meander_fill.py | 13 |
2 files changed, 188 insertions, 151 deletions
diff --git a/lib/elements/fill_stitch.py b/lib/elements/fill_stitch.py index 8f22278b..df990652 100644 --- a/lib/elements/fill_stitch.py +++ b/lib/elements/fill_stitch.py @@ -301,153 +301,6 @@ class FillStitch(EmbroideryElement): return self.get_float_param("staggers", 4) @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 original_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] - - return shgeo.MultiPolygon([(paths[0], paths[1:])]) - - @property - @cache - def shape(self): - shape = self._get_clipped_path() - - if self.shape_is_valid(shape): - return shape - - # Repair not valid shapes - logger = logging.getLogger('shapely.geos') - level = logger.level - logger.setLevel(logging.CRITICAL) - - valid_shape = make_valid(shape) - - logger.setLevel(level) - - if isinstance(valid_shape, shgeo.Polygon): - return shgeo.MultiPolygon([valid_shape]) - if isinstance(valid_shape, shgeo.LineString): - return shgeo.MultiPolygon([]) - if shape.area == 0: - return shgeo.MultiPolygon([]) - - polygons = [] - for polygon in valid_shape.geoms: - if isinstance(polygon, shgeo.Polygon): - polygons.append(polygon) - if isinstance(polygon, shgeo.MultiPolygon): - polygons.extend(polygon.geoms) - return shgeo.MultiPolygon(polygons) - - def _get_clipped_path(self): - if self.node.clip is None: - return self.original_shape - - from .element import EmbroideryElement - clip_element = EmbroideryElement(self.node.clip) - clip_element.paths.sort(key=lambda point_list: shgeo.Polygon(point_list).area, reverse=True) - polygon = shgeo.MultiPolygon([(clip_element.paths[0], clip_element.paths[1:])]) - try: - intersection = polygon.intersection(self.original_shape) - except TopologicalError: - return self.original_shape - - if isinstance(intersection, shgeo.Polygon): - return shgeo.MultiPolygon([intersection]) - - if isinstance(intersection, shgeo.MultiPolygon): - return intersection - - polygons = [] - if isinstance(intersection, shgeo.GeometryCollection): - for geom in intersection.geoms: - if isinstance(geom, shgeo.Polygon): - polygons.append(geom) - return shgeo.MultiPolygon([polygons]) - - 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) - yield InvalidShapeError((x, y)) - - def validation_warnings(self): # noqa: C901 - if not self.shape_is_valid(self.original_shape): - why = explain_validity(self.original_shape) - message, x, y = re.findall(r".+?(?=\[)|-?\d+(?:\.\d+)?", why) - if "Hole lies outside shell" in message: - yield UnconnectedWarning((x, y)) - else: - yield BorderCrossWarning((x, y)) - - for shape in self.shape.geoms: - if self.shape.area < 20: - label = self.node.get(INKSCAPE_LABEL) or self.node.get("id") - yield SmallShapeWarning(shape.centroid, label) - - if self.shrink_or_grow_shape(shape, self.expand, True).is_empty: - yield ExpandWarning(shape.centroid) - - if self.shrink_or_grow_shape(shape, -self.fill_underlay_inset, True).is_empty: - yield UnderlayInsetWarning(shape.centroid) - - # guided fill warnings - if self.fill_method == 'guided_fill': - 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'), tooltip=_('Length of stitches around the outline of the fill region used when moving from section to section. ' @@ -477,6 +330,31 @@ class FillStitch(EmbroideryElement): return max(self.get_float_param("running_stitch_tolerance_mm", 0.2), 0.01) @property + @param('repeats', + _('Repeats'), + tooltip=_('Defines how many times to run down and back along the path.'), + type='int', + default="1", + select_items=[('fill_method', 'meander_fill')], + sort_index=7) + def repeats(self): + return max(1, self.get_int_param("repeats", 1)) + + @property + @param('bean_stitch_repeats', + _('Bean stitch number of repeats'), + tooltip=_('Backtrack each stitch this many times. ' + 'A value of 1 would triple each stitch (forward, back, forward). ' + 'A value of 2 would quintuple each stitch, etc.\n\n' + 'A pattern with various repeats can be created with a list of values separated by a space.'), + type='str', + select_items=[('fill_method', 'meander_fill')], + default=0, + sort_index=8) + def bean_stitch_repeats(self): + return self.get_multiple_int_param("bean_stitch_repeats", "0") + + @property @param('fill_underlay', _('Underlay'), type='toggle', group=_('Fill Underlay'), default=True) def fill_underlay(self): return self.get_boolean_param("fill_underlay", default=True) @@ -601,6 +479,153 @@ class FillStitch(EmbroideryElement): def underlay_underpath(self): return self.get_boolean_param('underlay_underpath', True) + @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 original_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] + + return shgeo.MultiPolygon([(paths[0], paths[1:])]) + + @property + @cache + def shape(self): + shape = self._get_clipped_path() + + if self.shape_is_valid(shape): + return shape + + # Repair not valid shapes + logger = logging.getLogger('shapely.geos') + level = logger.level + logger.setLevel(logging.CRITICAL) + + valid_shape = make_valid(shape) + + logger.setLevel(level) + + if isinstance(valid_shape, shgeo.Polygon): + return shgeo.MultiPolygon([valid_shape]) + if isinstance(valid_shape, shgeo.LineString): + return shgeo.MultiPolygon([]) + if shape.area == 0: + return shgeo.MultiPolygon([]) + + polygons = [] + for polygon in valid_shape.geoms: + if isinstance(polygon, shgeo.Polygon): + polygons.append(polygon) + if isinstance(polygon, shgeo.MultiPolygon): + polygons.extend(polygon.geoms) + return shgeo.MultiPolygon(polygons) + + def _get_clipped_path(self): + if self.node.clip is None: + return self.original_shape + + from .element import EmbroideryElement + clip_element = EmbroideryElement(self.node.clip) + clip_element.paths.sort(key=lambda point_list: shgeo.Polygon(point_list).area, reverse=True) + polygon = shgeo.MultiPolygon([(clip_element.paths[0], clip_element.paths[1:])]) + try: + intersection = polygon.intersection(self.original_shape) + except TopologicalError: + return self.original_shape + + if isinstance(intersection, shgeo.Polygon): + return shgeo.MultiPolygon([intersection]) + + if isinstance(intersection, shgeo.MultiPolygon): + return intersection + + polygons = [] + if isinstance(intersection, shgeo.GeometryCollection): + for geom in intersection.geoms: + if isinstance(geom, shgeo.Polygon): + polygons.append(geom) + return shgeo.MultiPolygon([polygons]) + + 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) + yield InvalidShapeError((x, y)) + + def validation_warnings(self): # noqa: C901 + if not self.shape_is_valid(self.original_shape): + why = explain_validity(self.original_shape) + message, x, y = re.findall(r".+?(?=\[)|-?\d+(?:\.\d+)?", why) + if "Hole lies outside shell" in message: + yield UnconnectedWarning((x, y)) + else: + yield BorderCrossWarning((x, y)) + + for shape in self.shape.geoms: + if self.shape.area < 20: + label = self.node.get(INKSCAPE_LABEL) or self.node.get("id") + yield SmallShapeWarning(shape.centroid, label) + + if self.shrink_or_grow_shape(shape, self.expand, True).is_empty: + yield ExpandWarning(shape.centroid) + + if self.shrink_or_grow_shape(shape, -self.fill_underlay_inset, True).is_empty: + yield UnderlayInsetWarning(shape.centroid) + + # guided fill warnings + if self.fill_method == 'guided_fill': + 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 + def shrink_or_grow_shape(self, shape, amount, validate=False): new_shape = shape if amount: @@ -663,9 +688,7 @@ class FillStitch(EmbroideryElement): fill_shapes = self.fill_shape(shape) for i, fill_shape in enumerate(fill_shapes.geoms): - if self.fill_method == 'auto_fill': - stitch_groups.extend(self.do_auto_fill(fill_shape, previous_stitch_group, start, end)) - elif self.fill_method == 'contour_fill': + if self.fill_method == 'contour_fill': stitch_groups.extend(self.do_contour_fill(fill_shape, previous_stitch_group, start)) elif self.fill_method == 'guided_fill': stitch_groups.extend(self.do_guided_fill(fill_shape, previous_stitch_group, start, end)) @@ -673,6 +696,9 @@ class FillStitch(EmbroideryElement): stitch_groups.extend(self.do_meander_fill(fill_shape, shape, i, start, end)) elif self.fill_method == 'circular_fill': stitch_groups.extend(self.do_circular_fill(fill_shape, previous_stitch_group, start, end)) + else: + # auto_fill + stitch_groups.extend(self.do_auto_fill(fill_shape, previous_stitch_group, start, end)) except ExitThread: raise except Exception: diff --git a/lib/stitches/meander_fill.py b/lib/stitches/meander_fill.py index 08ff4999..f6106606 100644 --- a/lib/stitches/meander_fill.py +++ b/lib/stitches/meander_fill.py @@ -15,7 +15,7 @@ from ..utils.list import poprandom from ..utils.prng import iter_uniform_floats from ..utils.smoothing import smooth_path from ..utils.threading import check_stop_flag -from .running_stitch import running_stitch +from .running_stitch import bean_stitch, running_stitch def meander_fill(fill, shape, original_shape, shape_index, starting_point, ending_point): @@ -186,6 +186,17 @@ def post_process(points, shape, original_shape, fill): if fill.clip: stitches = clamp_path_to_polygon(stitches, original_shape) + if fill.bean_stitch_repeats: + stitches = bean_stitch(stitches, fill.bean_stitch_repeats) + + if fill.repeats: + for i in range(1, fill.repeats): + if i % 2 == 1: + # reverse every other pass + stitches.extend(stitches[::-1]) + else: + stitches.extend(stitches) + return stitches |
