summaryrefslogtreecommitdiff
path: root/lib/elements/fill_stitch.py
diff options
context:
space:
mode:
authorKaalleen <36401965+kaalleen@users.noreply.github.com>2023-04-24 22:52:31 +0200
committerGitHub <noreply@github.com>2023-04-24 22:52:31 +0200
commite5ccb10eef83b88b722e94dc39f8bcc7692a3ce1 (patch)
treeba17b2204c85532549b288d3f8338e7422162d69 /lib/elements/fill_stitch.py
parent9b726f6436c87860cc3aef1bb24c61fb9e4e06cd (diff)
Add bean stitch and repeat options to meander fill (#2232)
Diffstat (limited to 'lib/elements/fill_stitch.py')
-rw-r--r--lib/elements/fill_stitch.py326
1 files changed, 176 insertions, 150 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: