summaryrefslogtreecommitdiff
path: root/lib/elements
diff options
context:
space:
mode:
Diffstat (limited to 'lib/elements')
-rw-r--r--lib/elements/__init__.py3
-rw-r--r--lib/elements/auto_fill.py291
-rw-r--r--lib/elements/clone.py41
-rw-r--r--lib/elements/element.py30
-rw-r--r--lib/elements/empty_d_object.py4
-rw-r--r--lib/elements/fill.py202
-rw-r--r--lib/elements/fill_stitch.py637
-rw-r--r--lib/elements/marker.py (renamed from lib/elements/pattern.py)13
-rw-r--r--lib/elements/polyline.py21
-rw-r--r--lib/elements/satin_column.py96
-rw-r--r--lib/elements/stroke.py351
-rw-r--r--lib/elements/utils.py23
12 files changed, 1077 insertions, 635 deletions
diff --git a/lib/elements/__init__.py b/lib/elements/__init__.py
index 2e4c31a7..00933f36 100644
--- a/lib/elements/__init__.py
+++ b/lib/elements/__init__.py
@@ -3,11 +3,10 @@
# Copyright (c) 2010 Authors
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
-from .auto_fill import AutoFill
from .clone import Clone
from .element import EmbroideryElement
from .empty_d_object import EmptyDObject
-from .fill import Fill
+from .fill_stitch import FillStitch
from .image import ImageObject
from .polyline import Polyline
from .satin_column import SatinColumn
diff --git a/lib/elements/auto_fill.py b/lib/elements/auto_fill.py
deleted file mode 100644
index fbbd86d3..00000000
--- a/lib/elements/auto_fill.py
+++ /dev/null
@@ -1,291 +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 math
-import sys
-import traceback
-
-from shapely import geometry as shgeo
-
-from .element import param
-from .fill import Fill
-from .validation import ValidationWarning
-from ..i18n import _
-from ..stitch_plan import StitchGroup
-from ..stitches import auto_fill
-from ..svg.tags import INKSCAPE_LABEL
-from ..utils import cache, version
-
-
-class SmallShapeWarning(ValidationWarning):
- name = _("Small Fill")
- description = _("This fill object is so small that it would probably look better as running stitch or satin column. "
- "For very small shapes, fill stitch is not possible, and Ink/Stitch will use running stitch around "
- "the outline instead.")
-
-
-class ExpandWarning(ValidationWarning):
- name = _("Expand")
- description = _("The expand parameter for this fill object cannot be applied. "
- "Ink/Stitch will ignore it and will use original size instead.")
-
-
-class UnderlayInsetWarning(ValidationWarning):
- name = _("Inset")
- description = _("The underlay inset parameter for this fill object cannot be applied. "
- "Ink/Stitch will ignore it and will use the original size instead.")
-
-
-class AutoFill(Fill):
- 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)
-
- @property
- @cache
- def outline(self):
- return self.shape.boundary[0]
-
- @property
- @cache
- def outline_length(self):
- return self.outline.length
-
- @property
- def flip(self):
- return False
-
- @property
- @param('running_stitch_length_mm',
- _('Running stitch length (traversal between sections)'),
- 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)
- def running_stitch_length(self):
- return max(self.get_float_param("running_stitch_length_mm", 1.5), 0.01)
-
- @property
- @param('fill_underlay', _('Underlay'), type='toggle', group=_('AutoFill Underlay'), default=True)
- def fill_underlay(self):
- return self.get_boolean_param("fill_underlay", default=True)
-
- @property
- @param('fill_underlay_angle',
- _('Fill angle'),
- tooltip=_('Default: fill angle + 90 deg. Insert comma-seperated list for multiple layers.'),
- unit='deg',
- group=_('AutoFill Underlay'),
- type='float')
- @cache
- def fill_underlay_angle(self):
- underlay_angles = self.get_param('fill_underlay_angle', None)
- default_value = [self.angle + math.pi / 2.0]
- if underlay_angles is not None:
- underlay_angles = underlay_angles.strip().split(',')
- try:
- underlay_angles = [math.radians(float(angle)) for angle in underlay_angles]
- except (TypeError, ValueError):
- return default_value
- else:
- underlay_angles = default_value
-
- return underlay_angles
-
- @property
- @param('fill_underlay_row_spacing_mm',
- _('Row spacing'),
- tooltip=_('default: 3x fill row spacing'),
- unit='mm',
- group=_('AutoFill Underlay'),
- type='float')
- @cache
- def fill_underlay_row_spacing(self):
- return self.get_float_param("fill_underlay_row_spacing_mm") or self.row_spacing * 3
-
- @property
- @param('fill_underlay_max_stitch_length_mm',
- _('Max stitch length'),
- tooltip=_('default: equal to fill max stitch length'),
- unit='mm',
- group=_('AutoFill Underlay'), type='float')
- @cache
- def fill_underlay_max_stitch_length(self):
- return self.get_float_param("fill_underlay_max_stitch_length_mm") or self.max_stitch_length
-
- @property
- @param('fill_underlay_inset_mm',
- _('Inset'),
- tooltip=_('Shrink the shape before doing underlay, to prevent underlay from showing around the outside of the fill.'),
- unit='mm',
- group=_('AutoFill Underlay'),
- type='float',
- default=0)
- def fill_underlay_inset(self):
- return self.get_float_param('fill_underlay_inset_mm', 0)
-
- @property
- @param(
- 'fill_underlay_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.'),
- group=_('AutoFill Underlay'),
- type='boolean',
- default=False)
- def fill_underlay_skip_last(self):
- return self.get_boolean_param("fill_underlay_skip_last", False)
-
- @property
- @param('expand_mm',
- _('Expand'),
- tooltip=_('Expand the shape before fill stitching, to compensate for gaps between shapes.'),
- unit='mm',
- type='float',
- default=0)
- def expand(self):
- return self.get_float_param('expand_mm', 0)
-
- @property
- @param('underpath',
- _('Underpath'),
- tooltip=_('Travel inside the shape when moving from section to section. Underpath '
- '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)
- def underpath(self):
- return self.get_boolean_param('underpath', True)
-
- @property
- @param(
- 'underlay_underpath',
- _('Underpath'),
- tooltip=_('Travel inside the shape when moving from section to section. Underpath '
- 'stitches avoid traveling in the direction of the row angle so that they '
- 'are not visible. This gives them a jagged appearance.'),
- group=_('AutoFill Underlay'),
- type='boolean',
- default=True)
- def underlay_underpath(self):
- return self.get_boolean_param('underlay_underpath', True)
-
- def shrink_or_grow_shape(self, amount, validate=False):
- if amount:
- shape = self.shape.buffer(amount)
- # changing the size can empty the shape
- # in this case we want to use the original shape rather than returning an error
- if shape.is_empty and not validate:
- return self.shape
- if not isinstance(shape, shgeo.MultiPolygon):
- shape = shgeo.MultiPolygon([shape])
- return shape
- else:
- return self.shape
-
- @property
- def underlay_shape(self):
- return self.shrink_or_grow_shape(-self.fill_underlay_inset)
-
- @property
- def fill_shape(self):
- return self.shrink_or_grow_shape(self.expand)
-
- def get_starting_point(self, last_patch):
- # If there is a "fill_start" Command, then use that; otherwise pick
- # the point closest to the end of the last patch.
-
- if self.get_command('fill_start'):
- return self.get_command('fill_start').target_point
- elif last_patch:
- return last_patch.stitches[-1]
- else:
- return None
-
- def get_ending_point(self):
- if self.get_command('fill_end'):
- return self.get_command('fill_end').target_point
- else:
- return None
-
- def to_stitch_groups(self, last_patch):
- stitch_groups = []
-
- starting_point = self.get_starting_point(last_patch)
- ending_point = self.get_ending_point()
-
- try:
- if self.fill_underlay:
- for i in range(len(self.fill_underlay_angle)):
- underlay = StitchGroup(
- color=self.color,
- tags=("auto_fill", "auto_fill_underlay"),
- stitches=auto_fill(
- self.underlay_shape,
- self.fill_underlay_angle[i],
- self.fill_underlay_row_spacing,
- self.fill_underlay_row_spacing,
- self.fill_underlay_max_stitch_length,
- self.running_stitch_length,
- self.staggers,
- self.fill_underlay_skip_last,
- starting_point,
- underpath=self.underlay_underpath))
- stitch_groups.append(underlay)
-
- 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
- raise
-
- # for an uncaught exception, give a little more info so that they can create a bug report
- message = ""
- message += _("Error during autofill! This means that there is a problem with Ink/Stitch.")
- message += "\n\n"
- # L10N this message is followed by a URL: https://github.com/inkstitch/inkstitch/issues/new
- message += _("If you'd like to help us make Ink/Stitch better, please paste this whole message into a new issue at: ")
- message += "https://github.com/inkstitch/inkstitch/issues/new\n\n"
- message += version.get_inkstitch_version() + "\n\n"
- message += traceback.format_exc()
-
- self.fatal(message)
-
- return stitch_groups
-
-
-def validation_warnings(self):
- if self.shape.area < 20:
- label = self.node.get(INKSCAPE_LABEL) or self.node.get("id")
- yield SmallShapeWarning(self.shape.centroid, label)
-
- if self.shrink_or_grow_shape(self.expand, True).is_empty:
- yield ExpandWarning(self.shape.centroid)
-
- if self.shrink_or_grow_shape(-self.fill_underlay_inset, True).is_empty:
- yield UnderlayInsetWarning(self.shape.centroid)
-
- for warning in super(AutoFill, self).validation_warnings():
- yield warning
diff --git a/lib/elements/clone.py b/lib/elements/clone.py
index a9e10d94..d9185012 100644
--- a/lib/elements/clone.py
+++ b/lib/elements/clone.py
@@ -5,21 +5,14 @@
from math import atan, degrees
-import inkex
-
-from ..commands import is_command, is_command_symbol
+from ..commands import is_command_symbol
from ..i18n import _
from ..svg.path import get_node_transform
from ..svg.svg import find_elements
-from ..svg.tags import (EMBROIDERABLE_TAGS, INKSTITCH_ATTRIBS,
- SVG_POLYLINE_TAG, SVG_USE_TAG, XLINK_HREF)
+from ..svg.tags import (EMBROIDERABLE_TAGS, INKSTITCH_ATTRIBS, SVG_USE_TAG,
+ XLINK_HREF)
from ..utils import cache
-from .auto_fill import AutoFill
from .element import EmbroideryElement, param
-from .fill import Fill
-from .polyline import Polyline
-from .satin_column import SatinColumn
-from .stroke import Stroke
from .validation import ObjectTypeWarning, ValidationWarning
@@ -70,28 +63,8 @@ class Clone(EmbroideryElement):
return self.get_float_param('angle', 0)
def clone_to_element(self, node):
- # we need to determine if the source element is polyline, stroke, fill or satin
- element = EmbroideryElement(node)
-
- if node.tag == SVG_POLYLINE_TAG:
- return [Polyline(node)]
-
- elif element.get_boolean_param("satin_column") and self.get_clone_style("stroke", self.node):
- return [SatinColumn(node)]
- else:
- elements = []
- if element.get_style("fill", "black") and not element.get_style("stroke", 1) == "0":
- if element.get_boolean_param("auto_fill", True):
- elements.append(AutoFill(node))
- else:
- elements.append(Fill(node))
- if element.get_style("stroke", self.node) is not None:
- if not is_command(element.node):
- elements.append(Stroke(node))
- if element.get_boolean_param("stroke_first", False):
- elements.reverse()
-
- return elements
+ from .utils import node_to_elements
+ return node_to_elements(node, True)
def to_stitch_groups(self, last_patch=None):
patches = []
@@ -100,7 +73,7 @@ class Clone(EmbroideryElement):
if source_node.tag not in EMBROIDERABLE_TAGS:
return []
- self.node.style = source_node.composed_style()
+ self.node.style = source_node.specified_style()
# a. a custom set fill angle
# b. calculated rotation for the cloned fill element to look exactly as it's source
@@ -128,7 +101,7 @@ class Clone(EmbroideryElement):
return patches
def get_clone_style(self, style_name, node, default=None):
- style = inkex.styles.AttrFallbackStyle(node).get(style_name) or default
+ style = node.style[style_name] or default
return style
def center(self, source_node):
diff --git a/lib/elements/element.py b/lib/elements/element.py
index f06982b2..75d22580 100644
--- a/lib/elements/element.py
+++ b/lib/elements/element.py
@@ -20,7 +20,7 @@ from ..utils import Point, cache
class Param(object):
def __init__(self, name, description, unit=None, values=[], type=None, group=None, inverse=False,
- options=[], default=None, tooltip=None, sort_index=0):
+ options=[], default=None, tooltip=None, sort_index=0, select_items=None):
self.name = name
self.description = description
self.unit = unit
@@ -32,6 +32,7 @@ class Param(object):
self.default = default
self.tooltip = tooltip
self.sort_index = sort_index
+ self.select_items = select_items
def __repr__(self):
return "Param(%s)" % vars(self)
@@ -86,8 +87,11 @@ class EmbroideryElement(object):
return params
def replace_legacy_param(self, param):
- value = self.node.get(param, "").strip()
- self.set_param(param[10:], value)
+ # remove "embroider_" prefix
+ new_param = param[10:]
+ if new_param in INKSTITCH_ATTRIBS:
+ value = self.node.get(param, "").strip()
+ self.set_param(param[10:], value)
del self.node.attrib[param]
@cache
@@ -202,12 +206,24 @@ class EmbroideryElement(object):
# L10N options to allow lock stitch before and after objects
options=[_("Both"), _("Before"), _("After"), _("Neither")],
default=0,
- sort_index=4)
+ sort_index=50)
@cache
def ties(self):
return self.get_int_param("ties", 0)
@property
+ @param('force_lock_stitches',
+ _('Force lock stitches'),
+ tooltip=_('Sew lock stitches after sewing this element, '
+ 'even if the distance to the next object is shorter than defined by the collapse length value in the Ink/Stitch preferences.'),
+ type='boolean',
+ default=False,
+ sort_index=51)
+ @cache
+ def force_lock_stitches(self):
+ return self.get_boolean_param('force_lock_stitches', False)
+
+ @property
def path(self):
# A CSP is a "cubic superpath".
#
@@ -251,6 +267,11 @@ class EmbroideryElement(object):
return apply_transforms(self.path, self.node)
@property
+ @cache
+ def paths(self):
+ return self.flatten(self.parse_path())
+
+ @property
def shape(self):
raise NotImplementedError("INTERNAL ERROR: %s must implement shape()", self.__class__)
@@ -312,6 +333,7 @@ class EmbroideryElement(object):
for patch in patches:
patch.tie_modus = self.ties
+ patch.force_lock_stitches = self.force_lock_stitches
if patches:
patches[-1].trim_after = self.has_command("trim") or self.trim_after
diff --git a/lib/elements/empty_d_object.py b/lib/elements/empty_d_object.py
index 3c24f333..8f3b08de 100644
--- a/lib/elements/empty_d_object.py
+++ b/lib/elements/empty_d_object.py
@@ -23,5 +23,9 @@ class EmptyDObject(EmbroideryElement):
label = self.node.get(INKSCAPE_LABEL) or self.node.get("id")
yield EmptyD((0, 0), label)
+ @property
+ def shape(self):
+ return
+
def to_stitch_groups(self, last_patch):
return []
diff --git a/lib/elements/fill.py b/lib/elements/fill.py
deleted file mode 100644
index 442922b6..00000000
--- a/lib/elements/fill.py
+++ /dev/null
@@ -1,202 +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)
- # 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 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]
diff --git a/lib/elements/fill_stitch.py b/lib/elements/fill_stitch.py
new file mode 100644
index 00000000..f1a75e2f
--- /dev/null
+++ b/lib/elements/fill_stitch.py
@@ -0,0 +1,637 @@
+# 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
+import sys
+import traceback
+
+from shapely import geometry as shgeo
+from shapely.validation import explain_validity
+
+from ..i18n import _
+from ..marker import get_marker_elements
+from ..stitch_plan import StitchGroup
+from ..stitches import contour_fill, auto_fill, legacy_fill, guided_fill
+from ..svg import PIXELS_PER_MM
+from ..svg.tags import INKSCAPE_LABEL
+from ..utils import cache, version
+from .element import EmbroideryElement, param
+from .validation import ValidationError, ValidationWarning
+
+
+class SmallShapeWarning(ValidationWarning):
+ name = _("Small Fill")
+ description = _("This fill object is so small that it would probably look better as running stitch or satin column. "
+ "For very small shapes, fill stitch is not possible, and Ink/Stitch will use running stitch around "
+ "the outline instead.")
+
+
+class ExpandWarning(ValidationWarning):
+ name = _("Expand")
+ description = _("The expand parameter for this fill object cannot be applied. "
+ "Ink/Stitch will ignore it and will use original size instead.")
+
+
+class UnderlayInsetWarning(ValidationWarning):
+ name = _("Inset")
+ description = _("The underlay inset parameter for this fill object cannot be applied. "
+ "Ink/Stitch will ignore it and will use the original size instead.")
+
+
+class MissingGuideLineWarning(ValidationWarning):
+ name = _("Missing Guideline")
+ description = _('This object is set to "Guided Fill", but has no guide line.')
+ steps_to_solve = [
+ _('* Create a stroke object'),
+ _('* Select this object and run Extensions > Ink/Stitch > Edit > Selection to guide line')
+ ]
+
+
+class DisjointGuideLineWarning(ValidationWarning):
+ name = _("Disjointed Guide Line")
+ description = _("The guide line of this object isn't within the object borders. "
+ "The guide line works best, if it is within the target element.")
+ steps_to_solve = [
+ _('* Move the guide line into the element')
+ ]
+
+
+class MultipleGuideLineWarning(ValidationWarning):
+ name = _("Multiple Guide Lines")
+ description = _("This object has multiple guide lines, but only the first one will be used.")
+ steps_to_solve = [
+ _("* Remove all guide lines, except for one.")
+ ]
+
+
+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 FillStitch(EmbroideryElement):
+ element_name = _("FillStitch")
+
+ @property
+ @param('auto_fill', _('Automatically routed fill stitching'), type='toggle', default=True, sort_index=1)
+ def auto_fill(self):
+ return self.get_boolean_param('auto_fill', True)
+
+ @property
+ @param('fill_method', _('Fill method'), type='dropdown', default=0,
+ options=[_("Auto Fill"), _("Contour Fill"), _("Guided Fill"), _("Legacy Fill")], sort_index=2)
+ def fill_method(self):
+ return self.get_int_param('fill_method', 0)
+
+ @property
+ @param('contour_strategy', _('Contour Fill Strategy'), type='dropdown', default=0,
+ options=[_("Inner to Outer"), _("Single spiral"), _("Double spiral")], select_items=[('fill_method', 1)], sort_index=3)
+ def contour_strategy(self):
+ return self.get_int_param('contour_strategy', 0)
+
+ @property
+ @param('join_style', _('Join Style'), type='dropdown', default=0,
+ options=[_("Round"), _("Mitered"), _("Beveled")], select_items=[('fill_method', 1)], sort_index=4)
+ def join_style(self):
+ return self.get_int_param('join_style', 0)
+
+ @property
+ @param('avoid_self_crossing', _('Avoid self-crossing'), type='boolean', default=False, select_items=[('fill_method', 1)], sort_index=5)
+ def avoid_self_crossing(self):
+ return self.get_boolean_param('avoid_self_crossing', False)
+
+ @property
+ @param('clockwise', _('Clockwise'), type='boolean', default=True, select_items=[('fill_method', 1)], sort_index=5)
+ def clockwise(self):
+ return self.get_boolean_param('clockwise', 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=6,
+ select_items=[('fill_method', 0), ('fill_method', 3)],
+ 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=6,
+ select_items=[('fill_method', 0), ('fill_method', 2),
+ ('fill_method', 3)],
+ 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=7,
+ select_items=[('fill_method', 3)],
+ 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=6,
+ 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=6,
+ 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=6,
+ select_items=[('fill_method', 0), ('fill_method', 3)],
+ 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 validation_warnings(self):
+ if self.shape.area < 20:
+ label = self.node.get(INKSCAPE_LABEL) or self.node.get("id")
+ yield SmallShapeWarning(self.shape.centroid, label)
+
+ if self.shrink_or_grow_shape(self.expand, True).is_empty:
+ yield ExpandWarning(self.shape.centroid)
+
+ if self.shrink_or_grow_shape(-self.fill_underlay_inset, True).is_empty:
+ yield UnderlayInsetWarning(self.shape.centroid)
+
+ # guided fill warnings
+ if self.fill_method == 2:
+ 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 (traversal between sections)'),
+ 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,
+ select_items=[('fill_method', 0), ('fill_method', 2)],
+ sort_index=6)
+ def running_stitch_length(self):
+ return max(self.get_float_param("running_stitch_length_mm", 1.5), 0.01)
+
+ @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)
+
+ @property
+ @param('fill_underlay_angle',
+ _('Fill angle'),
+ tooltip=_('Default: fill angle + 90 deg. Insert comma-seperated list for multiple layers.'),
+ unit='deg',
+ group=_('Fill Underlay'),
+ type='float')
+ @cache
+ def fill_underlay_angle(self):
+ underlay_angles = self.get_param('fill_underlay_angle', None)
+ default_value = [self.angle + math.pi / 2.0]
+ if underlay_angles is not None:
+ underlay_angles = underlay_angles.strip().split(',')
+ try:
+ underlay_angles = [math.radians(
+ float(angle)) for angle in underlay_angles]
+ except (TypeError, ValueError):
+ return default_value
+ else:
+ underlay_angles = default_value
+
+ return underlay_angles
+
+ @property
+ @param('fill_underlay_row_spacing_mm',
+ _('Row spacing'),
+ tooltip=_('default: 3x fill row spacing'),
+ unit='mm',
+ group=_('Fill Underlay'),
+ type='float')
+ @cache
+ def fill_underlay_row_spacing(self):
+ return self.get_float_param("fill_underlay_row_spacing_mm") or self.row_spacing * 3
+
+ @property
+ @param('fill_underlay_max_stitch_length_mm',
+ _('Max stitch length'),
+ tooltip=_('default: equal to fill max stitch length'),
+ unit='mm',
+ group=_('Fill Underlay'), type='float')
+ @cache
+ def fill_underlay_max_stitch_length(self):
+ return self.get_float_param("fill_underlay_max_stitch_length_mm") or self.max_stitch_length
+
+ @property
+ @param('fill_underlay_inset_mm',
+ _('Inset'),
+ tooltip=_('Shrink the shape before doing underlay, to prevent underlay from showing around the outside of the fill.'),
+ unit='mm',
+ group=_('Fill Underlay'),
+ type='float',
+ default=0)
+ def fill_underlay_inset(self):
+ return self.get_float_param('fill_underlay_inset_mm', 0)
+
+ @property
+ @param(
+ 'fill_underlay_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.'),
+ group=_('Fill Underlay'),
+ type='boolean',
+ default=False)
+ def fill_underlay_skip_last(self):
+ return self.get_boolean_param("fill_underlay_skip_last", False)
+
+ @property
+ @param('expand_mm',
+ _('Expand'),
+ tooltip=_('Expand the shape before fill stitching, to compensate for gaps between shapes.'),
+ unit='mm',
+ type='float',
+ default=0,
+ sort_index=5,
+ select_items=[('fill_method', 0), ('fill_method', 2)])
+ def expand(self):
+ return self.get_float_param('expand_mm', 0)
+
+ @property
+ @param('underpath',
+ _('Underpath'),
+ tooltip=_('Travel inside the shape when moving from section to section. Underpath '
+ '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,
+ select_items=[('fill_method', 0), ('fill_method', 2)],
+ sort_index=6)
+ def underpath(self):
+ return self.get_boolean_param('underpath', True)
+
+ @property
+ @param(
+ 'underlay_underpath',
+ _('Underpath'),
+ tooltip=_('Travel inside the shape when moving from section to section. Underpath '
+ 'stitches avoid traveling in the direction of the row angle so that they '
+ 'are not visible. This gives them a jagged appearance.'),
+ group=_('Fill Underlay'),
+ type='boolean',
+ default=True)
+ def underlay_underpath(self):
+ return self.get_boolean_param('underlay_underpath', True)
+
+ def shrink_or_grow_shape(self, amount, validate=False):
+ if amount:
+ shape = self.shape.buffer(amount)
+ # changing the size can empty the shape
+ # in this case we want to use the original shape rather than returning an error
+ if shape.is_empty and not validate:
+ return self.shape
+ if not isinstance(shape, shgeo.MultiPolygon):
+ shape = shgeo.MultiPolygon([shape])
+ return shape
+ else:
+ return self.shape
+
+ @property
+ def underlay_shape(self):
+ return self.shrink_or_grow_shape(-self.fill_underlay_inset)
+
+ @property
+ def fill_shape(self):
+ return self.shrink_or_grow_shape(self.expand)
+
+ def get_starting_point(self, last_patch):
+ # If there is a "fill_start" Command, then use that; otherwise pick
+ # the point closest to the end of the last patch.
+
+ if self.get_command('fill_start'):
+ return self.get_command('fill_start').target_point
+ elif last_patch:
+ return last_patch.stitches[-1]
+ else:
+ return None
+
+ def get_ending_point(self):
+ if self.get_command('fill_end'):
+ return self.get_command('fill_end').target_point
+ else:
+ return None
+
+ def to_stitch_groups(self, last_patch):
+ # backwards compatibility: legacy_fill used to be inkstitch:auto_fill == False
+ if not self.auto_fill or self.fill_method == 3:
+ return self.do_legacy_fill()
+ else:
+ stitch_groups = []
+ start = self.get_starting_point(last_patch)
+ end = self.get_ending_point()
+
+ try:
+ if self.fill_underlay:
+ underlay_stitch_groups, start = self.do_underlay(start)
+ stitch_groups.extend(underlay_stitch_groups)
+ if self.fill_method == 0:
+ stitch_groups.extend(self.do_auto_fill(last_patch, start, end))
+ if self.fill_method == 1:
+ stitch_groups.extend(self.do_contour_fill(last_patch, start))
+ elif self.fill_method == 2:
+ stitch_groups.extend(self.do_guided_fill(last_patch, start, end))
+ except Exception:
+ self.fatal_fill_error()
+
+ return stitch_groups
+
+ def do_legacy_fill(self):
+ 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]
+
+ def do_underlay(self, starting_point):
+ stitch_groups = []
+ for i in range(len(self.fill_underlay_angle)):
+ underlay = StitchGroup(
+ color=self.color,
+ tags=("auto_fill", "auto_fill_underlay"),
+ stitches=auto_fill(
+ self.underlay_shape,
+ self.fill_underlay_angle[i],
+ self.fill_underlay_row_spacing,
+ self.fill_underlay_row_spacing,
+ self.fill_underlay_max_stitch_length,
+ self.running_stitch_length,
+ self.staggers,
+ self.fill_underlay_skip_last,
+ starting_point,
+ underpath=self.underlay_underpath))
+ stitch_groups.append(underlay)
+
+ starting_point = underlay.stitches[-1]
+ return [stitch_groups, starting_point]
+
+ def do_auto_fill(self, last_patch, starting_point, ending_point):
+ 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))
+ return [stitch_group]
+
+ def do_contour_fill(self, last_patch, starting_point):
+ if not starting_point:
+ starting_point = (0, 0)
+ starting_point = shgeo.Point(starting_point)
+
+ stitch_groups = []
+ for polygon in self.fill_shape.geoms:
+ tree = contour_fill.offset_polygon(polygon, self.row_spacing, self.join_style + 1, self.clockwise)
+
+ stitches = []
+ if self.contour_strategy == 0:
+ stitches = contour_fill.inner_to_outer(
+ tree,
+ self.row_spacing,
+ self.max_stitch_length,
+ starting_point,
+ self.avoid_self_crossing
+ )
+ elif self.contour_strategy == 1:
+ stitches = contour_fill.single_spiral(
+ tree,
+ self.max_stitch_length,
+ starting_point
+ )
+ elif self.contour_strategy == 2:
+ stitches = contour_fill.double_spiral(
+ tree,
+ self.max_stitch_length,
+ starting_point
+ )
+
+ stitch_group = StitchGroup(
+ color=self.color,
+ tags=("auto_fill", "auto_fill_top"),
+ stitches=stitches)
+ stitch_groups.append(stitch_group)
+
+ return stitch_groups
+
+ def do_guided_fill(self, last_patch, starting_point, ending_point):
+ guide_line = self._get_guide_lines()
+
+ # No guide line: fallback to normal autofill
+ if not guide_line:
+ return self.do_auto_fill(last_patch, starting_point, ending_point)
+
+ stitch_group = StitchGroup(
+ color=self.color,
+ tags=("guided_fill", "auto_fill_top"),
+ stitches=guided_fill(
+ self.fill_shape,
+ guide_line.geoms[0],
+ self.angle,
+ self.row_spacing,
+ self.max_stitch_length,
+ self.running_stitch_length,
+ self.skip_last,
+ starting_point,
+ ending_point,
+ self.underpath))
+ return [stitch_group]
+
+ @cache
+ def _get_guide_lines(self, multiple=False):
+ guide_lines = get_marker_elements(self.node, "guide-line", False, True)
+ # No or empty guide line
+ if not guide_lines or not guide_lines['stroke']:
+ return None
+
+ if multiple:
+ return guide_lines['stroke']
+ else:
+ return guide_lines['stroke'][0]
+
+ def fatal_fill_error(self):
+ if hasattr(sys, 'gettrace') and sys.gettrace():
+ # if we're debugging, let the exception bubble up
+ raise
+
+ # for an uncaught exception, give a little more info so that they can create a bug report
+ message = ""
+ message += _("Error during autofill! This means that there is a problem with Ink/Stitch.")
+ message += "\n\n"
+ # L10N this message is followed by a URL: https://github.com/inkstitch/inkstitch/issues/new
+ message += _("If you'd like to help us make Ink/Stitch better, please paste this whole message into a new issue at: ")
+ message += "https://github.com/inkstitch/inkstitch/issues/new\n\n"
+ message += version.get_inkstitch_version() + "\n\n"
+ message += traceback.format_exc()
+
+ self.fatal(message)
diff --git a/lib/elements/pattern.py b/lib/elements/marker.py
index 4b92d366..574ce91e 100644
--- a/lib/elements/pattern.py
+++ b/lib/elements/marker.py
@@ -10,24 +10,23 @@ from .element import EmbroideryElement
from .validation import ObjectTypeWarning
-class PatternWarning(ObjectTypeWarning):
- name = _("Pattern Element")
+class MarkerWarning(ObjectTypeWarning):
+ name = _("Marker Element")
description = _("This element will not be embroidered. "
- "It will appear as a pattern applied to objects in the same group as it. "
- "Objects in sub-groups will be ignored.")
+ "It will be applied to objects in the same group. Objects in sub-groups will be ignored.")
steps_to_solve = [
- _("To disable pattern mode, remove the pattern marker:"),
+ _("Turn back to normal embroidery element mode, remove the marker:"),
_('* Open the Fill and Stroke panel (Objects > Fill and Stroke)'),
_('* Go to the Stroke style tab'),
_('* Under "Markers" choose the first (empty) option in the first dropdown list.')
]
-class PatternObject(EmbroideryElement):
+class MarkerObject(EmbroideryElement):
def validation_warnings(self):
repr_point = next(inkex.Path(self.parse_path()).end_points)
- yield PatternWarning(repr_point)
+ yield MarkerWarning(repr_point)
def to_stitch_groups(self, last_patch):
return []
diff --git a/lib/elements/polyline.py b/lib/elements/polyline.py
index e923aac0..5086c705 100644
--- a/lib/elements/polyline.py
+++ b/lib/elements/polyline.py
@@ -47,19 +47,9 @@ class Polyline(EmbroideryElement):
return self.get_boolean_param("polyline")
@property
- def points(self):
- # example: "1,2 0,0 1.5,3 4,2"
-
- points = self.node.get('points').strip()
- points = points.split(" ")
- points = [[float(coord) for coord in point.split(",")] for point in points]
-
- return points
-
- @property
@cache
def shape(self):
- return shgeo.LineString(self.points)
+ return shgeo.LineString(self.path)
@property
def path(self):
@@ -68,9 +58,12 @@ class Polyline(EmbroideryElement):
# svg transforms that is in our superclass, we'll convert the polyline
# to a degenerate cubic superpath in which the bezier handles are on
# the segment endpoints.
- path = self.node.get_path()
+ if self.node.get('points', None):
+ path = self.node.get_path()
+ else:
+ # Set path to (0, 0) for empty polylines
+ path = 'M 0 0'
path = Path(path).to_superpath()
-
return path
@property
@@ -99,7 +92,7 @@ class Polyline(EmbroideryElement):
return stitches
def validation_warnings(self):
- yield PolylineWarning(self.points[0])
+ yield PolylineWarning(self.path[0][0][0])
def to_stitch_groups(self, last_patch):
patch = StitchGroup(color=self.color)
diff --git a/lib/elements/satin_column.py b/lib/elements/satin_column.py
index cf31c2af..b944bee5 100644
--- a/lib/elements/satin_column.py
+++ b/lib/elements/satin_column.py
@@ -6,17 +6,18 @@
from copy import deepcopy
from itertools import chain
-from inkex import paths
from shapely import affinity as shaffinity
from shapely import geometry as shgeo
from shapely.ops import nearest_points
-from .element import EmbroideryElement, param
-from .validation import ValidationError, ValidationWarning
+from inkex import paths
+
from ..i18n import _
from ..stitch_plan import StitchGroup
from ..svg import line_strings_to_csp, point_lists_to_csp
from ..utils import Point, cache, collapse_duplicate_point, cut
+from .element import EmbroideryElement, param
+from .validation import ValidationError, ValidationWarning
class SatinHasFillError(ValidationError):
@@ -51,6 +52,15 @@ class UnequalPointsError(ValidationError):
]
+class NotStitchableError(ValidationError):
+ name = _("Not stitchable satin column")
+ description = _("A satin column consists out of two rails and one or more rungs. This satin column may have a different setup.")
+ steps_to_solve = [
+ _('Make sure your satin column is not a combination of multiple satin columns.'),
+ _('Go to our website and read how a satin column should look like https://inkstitch.org/docs/stitches/satin-column/'),
+ ]
+
+
rung_message = _("Each rung should intersect both rails once.")
@@ -156,6 +166,11 @@ class SatinColumn(EmbroideryElement):
return max(self.get_float_param("center_walk_underlay_stitch_length_mm", 1.5), 0.01)
@property
+ @param('center_walk_underlay_repeats', _('Repeats'), group=_('Center-Walk Underlay'), type='int', default=2, sort_index=2)
+ def center_walk_underlay_repeats(self):
+ return max(self.get_int_param("center_walk_underlay_repeats", 2), 1)
+
+ @property
@param('zigzag_underlay', _('Zig-zag underlay'), type='toggle', group=_('Zig-zag Underlay'))
def zigzag_underlay(self):
return self.get_boolean_param("zigzag_underlay")
@@ -190,15 +205,23 @@ class SatinColumn(EmbroideryElement):
return self.get_float_param("zigzag_underlay_inset_mm") or self.contour_underlay_inset / 2.0
@property
+ @param('zigzag_underlay_max_stitch_length_mm',
+ _('Maximum stitch length'),
+ tooltip=_('Split stitch if distance of maximum stitch length is exceeded'),
+ unit='mm',
+ group=_('Zig-zag Underlay'),
+ type='float',
+ default="")
+ def zigzag_underlay_max_stitch_length(self):
+ return self.get_float_param("zigzag_underlay_max_stitch_length_mm") or None
+
+ @property
@cache
def shape(self):
# This isn't used for satins at all, but other parts of the code
# may need to know the general shape of a satin column.
- flattened = self.flatten(self.parse_path())
- line_strings = [shgeo.LineString(path) for path in flattened]
-
- return shgeo.MultiLineString(line_strings)
+ return shgeo.MultiLineString(self.flattened_rails).convex_hull
@property
@cache
@@ -411,6 +434,12 @@ class SatinColumn(EmbroideryElement):
if not intersection.is_empty and not isinstance(intersection, shgeo.Point):
yield TooManyIntersectionsError(rung.interpolate(0.5, normalized=True))
+ if not self.to_stitch_groups():
+ yield NotStitchableError(self.shape.centroid)
+
+ def _center_walk_is_odd(self):
+ return self.center_walk_underlay_repeats % 2 == 1
+
def reverse(self):
"""Return a new SatinColumn like this one but in the opposite direction.
@@ -715,24 +744,34 @@ class SatinColumn(EmbroideryElement):
def do_contour_underlay(self):
# "contour walk" underlay: do stitches up one side and down the
# other.
- forward, back = self.plot_points_on_rails(self.contour_underlay_stitch_length,
- -self.contour_underlay_inset)
+ forward, back = self.plot_points_on_rails(self.contour_underlay_stitch_length, -self.contour_underlay_inset)
+ stitches = (forward + list(reversed(back)))
+ if self._center_walk_is_odd():
+ stitches = (list(reversed(back)) + forward)
+
return StitchGroup(
color=self.color,
tags=("satin_column", "satin_column_underlay", "satin_contour_underlay"),
- stitches=(forward + list(reversed(back))))
+ stitches=stitches)
def do_center_walk(self):
# Center walk underlay is just a running stitch down and back on the
# center line between the bezier curves.
# Do it like contour underlay, but inset all the way to the center.
- forward, back = self.plot_points_on_rails(self.center_walk_underlay_stitch_length,
- -100000)
+ forward, back = self.plot_points_on_rails(self.center_walk_underlay_stitch_length, -100000)
+
+ stitches = []
+ for i in range(self.center_walk_underlay_repeats):
+ if i % 2 == 0:
+ stitches += forward
+ else:
+ stitches += list(reversed(back))
+
return StitchGroup(
color=self.color,
tags=("satin_column", "satin_column_underlay", "satin_center_walk"),
- stitches=(forward + list(reversed(back))))
+ stitches=stitches)
def do_zigzag_underlay(self):
# zigzag underlay, usually done at a much lower density than the
@@ -750,6 +789,9 @@ class SatinColumn(EmbroideryElement):
sides = self.plot_points_on_rails(self.zigzag_underlay_spacing / 2.0,
-self.zigzag_underlay_inset)
+ if self._center_walk_is_odd():
+ sides = [list(reversed(sides[0])), list(reversed(sides[1]))]
+
# This organizes the points in each side in the order that they'll be
# visited.
sides = [sides[0][::2] + list(reversed(sides[0][1::2])),
@@ -757,7 +799,14 @@ class SatinColumn(EmbroideryElement):
# This fancy bit of iterable magic just repeatedly takes a point
# from each side in turn.
+ last_point = None
for point in chain.from_iterable(zip(*sides)):
+ if last_point and self.zigzag_underlay_max_stitch_length:
+ if last_point.distance(point) > self.zigzag_underlay_max_stitch_length:
+ points, count = self._get_split_points(last_point, point, self.zigzag_underlay_max_stitch_length)
+ for point in points:
+ patch.add_stitch(point)
+ last_point = point
patch.add_stitch(point)
patch.add_tags(("satin_column", "satin_column_underlay", "satin_zigzag_underlay"))
@@ -783,6 +832,9 @@ class SatinColumn(EmbroideryElement):
for point in chain.from_iterable(zip(*sides)):
patch.add_stitch(point)
+ if self._center_walk_is_odd():
+ patch.stitches = list(reversed(patch.stitches))
+
patch.add_tags(("satin_column", "satin_column_edge"))
return patch
@@ -805,6 +857,9 @@ class SatinColumn(EmbroideryElement):
patch.add_stitch(right)
patch.add_stitch(left)
+ if self._center_walk_is_odd():
+ patch.stitches = list(reversed(patch.stitches))
+
patch.add_tags(("satin_column", "e_stitch"))
return patch
@@ -815,7 +870,7 @@ class SatinColumn(EmbroideryElement):
for i, (left, right) in enumerate(zip(*sides)):
patch.add_stitch(left)
patch.stitches[-1].add_tags(("satin_column", "satin_column_edge"))
- points, count = self._get_split_points(left, right)
+ points, count = self._get_split_points(left, right, self.max_stitch_length)
for point in points:
patch.add_stitch(point)
patch.stitches[-1].add_tags(("satin_column", "satin_split_stitch"))
@@ -825,23 +880,25 @@ class SatinColumn(EmbroideryElement):
# but it looks ugly if the points differ too much
# so let's make sure they have at least the same amount of divisions
if not i+1 >= len(sides[0]):
- points, count = self._get_split_points(right, sides[0][i+1], count)
+ points, count = self._get_split_points(right, sides[0][i+1], self.max_stitch_length, count)
for point in points:
patch.add_stitch(point)
patch.stitches[-1].add_tags(("satin_column", "satin_split_stitch"))
+ if self._center_walk_is_odd():
+ patch.stitches = list(reversed(patch.stitches))
return patch
- def _get_split_points(self, left, right, count=None):
+ def _get_split_points(self, left, right, max_stitch_length, count=None):
points = []
distance = left.distance(right)
- split_count = count or int(-(-distance // self.max_stitch_length))
+ split_count = count or int(-(-distance // max_stitch_length))
for i in range(split_count):
line = shgeo.LineString((left, right))
split_point = line.interpolate((i+1)/split_count, normalized=True)
points.append(Point(split_point.x, split_point.y))
return [points, split_count]
- def to_stitch_groups(self, last_patch):
+ def to_stitch_groups(self, last_patch=None):
# Stitch a variable-width satin column, zig-zagging between two paths.
# The algorithm will draw zigzags between each consecutive pair of
@@ -866,4 +923,7 @@ class SatinColumn(EmbroideryElement):
else:
patch += self.do_satin()
+ if not patch.stitches:
+ return []
+
return [patch]
diff --git a/lib/elements/stroke.py b/lib/elements/stroke.py
index 763167ad..6edd2c9e 100644
--- a/lib/elements/stroke.py
+++ b/lib/elements/stroke.py
@@ -7,16 +7,38 @@ import sys
import shapely.geometry
-from .element import EmbroideryElement, param
+from inkex import Transform
+
from ..i18n import _
+from ..marker import get_marker_elements
from ..stitch_plan import StitchGroup
from ..stitches import bean_stitch, running_stitch
-from ..svg import parse_length_with_units
+from ..stitches.ripple_stitch import ripple_stitch
+from ..svg import get_node_transform, parse_length_with_units
from ..utils import Point, cache
+from .element import EmbroideryElement, param
+from .validation import ValidationWarning
warned_about_legacy_running_stitch = False
+class IgnoreSkipValues(ValidationWarning):
+ name = _("Ignore skip")
+ description = _("Skip values are ignored, because there was no line left to embroider.")
+ steps_to_solve = [
+ _('* Open the params dialog with this object selected'),
+ _('* Reduce Skip values or increase number of lines'),
+ ]
+
+
+class MultipleGuideLineWarning(ValidationWarning):
+ name = _("Multiple Guide Lines")
+ description = _("This object has multiple guide lines, but only the first one will be used.")
+ steps_to_solve = [
+ _("* Remove all guide lines, except for one.")
+ ]
+
+
class Stroke(EmbroideryElement):
element_name = _("Stroke")
@@ -34,15 +56,36 @@ class Stroke(EmbroideryElement):
return self.get_style("stroke-dasharray") is not None
@property
- @param('running_stitch_length_mm',
- _('Running stitch length'),
- tooltip=_('Length of stitches in running stitch mode.'),
- unit='mm',
- type='float',
- default=1.5,
- sort_index=3)
- def running_stitch_length(self):
- return max(self.get_float_param("running_stitch_length_mm", 1.5), 0.01)
+ @param('stroke_method',
+ _('Method'),
+ type='dropdown',
+ default=0,
+ # 0: run/simple satin, 1: manual, 2: ripple
+ options=[_("Running Stitch"), _("Ripple")],
+ sort_index=0)
+ def stroke_method(self):
+ return self.get_int_param('stroke_method', 0)
+
+ @property
+ @param('manual_stitch',
+ _('Manual stitch placement'),
+ tooltip=_("Stitch every node in the path. All other options are ignored."),
+ type='boolean',
+ default=False,
+ select_items=[('stroke_method', 0)],
+ sort_index=1)
+ def manual_stitch_mode(self):
+ return self.get_boolean_param('manual_stitch')
+
+ @property
+ @param('repeats',
+ _('Repeats'),
+ tooltip=_('Defines how many times to run down and back along the path.'),
+ type='int',
+ default="1",
+ sort_index=2)
+ def repeats(self):
+ return max(1, self.get_int_param("repeats", 1))
@property
@param(
@@ -50,34 +93,196 @@ class Stroke(EmbroideryElement):
_('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. Only applies to running stitch.'),
+ 'A value of 2 would quintuple each stitch, etc.'),
type='int',
default=0,
- sort_index=2)
+ sort_index=3)
def bean_stitch_repeats(self):
return self.get_int_param("bean_stitch_repeats", 0)
@property
+ @param('running_stitch_length_mm',
+ _('Running stitch length'),
+ tooltip=_('Length of stitches in running stitch mode.'),
+ unit='mm',
+ type='float',
+ default=1.5,
+ sort_index=4)
+ def running_stitch_length(self):
+ return max(self.get_float_param("running_stitch_length_mm", 1.5), 0.01)
+
+ @property
@param('zigzag_spacing_mm',
_('Zig-zag spacing (peak-to-peak)'),
tooltip=_('Length of stitches in zig-zag mode.'),
unit='mm',
type='float',
default=0.4,
- sort_index=3)
+ select_items=[('stroke_method', 0)],
+ sort_index=5)
@cache
def zigzag_spacing(self):
return max(self.get_float_param("zigzag_spacing_mm", 0.4), 0.01)
@property
- @param('repeats',
- _('Repeats'),
- tooltip=_('Defines how many times to run down and back along the path.'),
+ @param('line_count',
+ _('Number of lines'),
+ tooltip=_('Number of lines from start to finish'),
type='int',
- default="1",
- sort_index=1)
- def repeats(self):
- return self.get_int_param("repeats", 1)
+ default=10,
+ select_items=[('stroke_method', 1)],
+ sort_index=5)
+ @cache
+ def line_count(self):
+ return max(self.get_int_param("line_count", 10), 1)
+
+ def get_line_count(self):
+ if self.is_closed:
+ return self.line_count + 1
+ return self.line_count
+
+ @property
+ @param('skip_start',
+ _('Skip first lines'),
+ tooltip=_('Skip this number of lines at the beginning.'),
+ type='int',
+ default=0,
+ select_items=[('stroke_method', 1)],
+ sort_index=6)
+ @cache
+ def skip_start(self):
+ return abs(self.get_int_param("skip_start", 0))
+
+ @property
+ @param('skip_end',
+ _('Skip last lines'),
+ tooltip=_('Skip this number of lines at the end'),
+ type='int',
+ default=0,
+ select_items=[('stroke_method', 1)],
+ sort_index=7)
+ @cache
+ def skip_end(self):
+ return abs(self.get_int_param("skip_end", 0))
+
+ def _adjust_skip(self, skip):
+ if self.skip_start + self.skip_end >= self.line_count:
+ return 0
+ else:
+ return skip
+
+ def get_skip_start(self):
+ return self._adjust_skip(self.skip_start)
+
+ def get_skip_end(self):
+ return self._adjust_skip(self.skip_end)
+
+ @property
+ @param('exponent',
+ _('Line distance exponent'),
+ tooltip=_('Increase density towards one side.'),
+ type='float',
+ default=1,
+ select_items=[('stroke_method', 1)],
+ sort_index=8)
+ @cache
+ def exponent(self):
+ return max(self.get_float_param("exponent", 1), 0.1)
+
+ @property
+ @param('flip_exponent',
+ _('Flip exponent'),
+ tooltip=_('Reverse exponent effect.'),
+ type='boolean',
+ default=False,
+ select_items=[('stroke_method', 1)],
+ sort_index=9)
+ @cache
+ def flip_exponent(self):
+ return self.get_boolean_param("flip_exponent", False)
+
+ @property
+ @param('reverse',
+ _('Reverse'),
+ tooltip=_('Flip start and end point'),
+ type='boolean',
+ default=False,
+ select_items=[('stroke_method', 1)],
+ sort_index=10)
+ @cache
+ def reverse(self):
+ return self.get_boolean_param("reverse", False)
+
+ @property
+ @param('grid_size',
+ _('Grid size'),
+ tooltip=_('Render as grid. Use with care and watch your stitch density.'),
+ type='float',
+ default=0,
+ unit='mm',
+ select_items=[('stroke_method', 1)],
+ sort_index=11)
+ @cache
+ def grid_size(self):
+ return abs(self.get_float_param("grid_size", 0))
+
+ @property
+ @param('scale_axis',
+ _('Scale axis'),
+ tooltip=_('Scale axis for satin guided ripple stitches.'),
+ type='dropdown',
+ default=0,
+ # 0: xy, 1: x, 2: y, 3: none
+ options=[_("X Y"), _("X"), _("Y"), _("None")],
+ select_items=[('stroke_method', 1)],
+ sort_index=12)
+ def scale_axis(self):
+ return self.get_int_param('scale_axis', 0)
+
+ @property
+ @param('scale_start',
+ _('Starting scale'),
+ tooltip=_('How big the first copy of the line should be, in percent.') + " " + _('Used only for ripple stitch with a guide line.'),
+ type='float',
+ default=100,
+ select_items=[('stroke_method', 1)],
+ sort_index=13)
+ def scale_start(self):
+ return self.get_float_param('scale_start', 100.0)
+
+ @property
+ @param('scale_end',
+ _('Ending scale'),
+ tooltip=_('How big the last copy of the line should be, in percent.') + " " + _('Used only for ripple stitch with a guide line.'),
+ type='float',
+ default=0.0,
+ select_items=[('stroke_method', 1)],
+ sort_index=14)
+ def scale_end(self):
+ return self.get_float_param('scale_end', 0.0)
+
+ @property
+ @param('rotate_ripples',
+ _('Rotate'),
+ tooltip=_('Rotate satin guided ripple stitches'),
+ type='boolean',
+ default=True,
+ select_items=[('stroke_method', 1)],
+ sort_index=15)
+ @cache
+ def rotate_ripples(self):
+ return self.get_boolean_param("rotate_ripples", True)
+
+ @property
+ @cache
+ def is_closed(self):
+ # returns true if the outline of a single line stroke is a closed shape
+ # (with a small tolerance)
+ lines = self.as_multi_line_string().geoms
+ if len(lines) == 1:
+ coords = lines[0].coords
+ return Point(*coords[0]).distance(Point(*coords[-1])) < 0.05
+ return False
@property
def paths(self):
@@ -86,7 +291,7 @@ class Stroke(EmbroideryElement):
# manipulate invalid path
if len(flattened[0]) == 1:
- return [[[flattened[0][0][0], flattened[0][0][1]], [flattened[0][0][0]+1.0, flattened[0][0][1]]]]
+ return [[[flattened[0][0][0], flattened[0][0][1]], [flattened[0][0][0] + 1.0, flattened[0][0][1]]]]
if self.manual_stitch_mode:
return [self.strip_control_points(subpath) for subpath in path]
@@ -96,22 +301,22 @@ class Stroke(EmbroideryElement):
@property
@cache
def shape(self):
- line_strings = [shapely.geometry.LineString(path) for path in self.paths]
-
- # Using convex_hull here is an important optimization. Otherwise
- # complex paths cause operations on the shape to take a long time.
- # This especially happens when importing machine embroidery files.
- return shapely.geometry.MultiLineString(line_strings).convex_hull
+ return self.as_multi_line_string().convex_hull
- @property
- @param('manual_stitch',
- _('Manual stitch placement'),
- tooltip=_("Stitch every node in the path. Stitch length and zig-zag spacing are ignored."),
- type='boolean',
- default=False,
- sort_index=0)
- def manual_stitch_mode(self):
- return self.get_boolean_param('manual_stitch')
+ @cache
+ def as_multi_line_string(self):
+ line_strings = [shapely.geometry.LineString(path) for path in self.paths]
+ return shapely.geometry.MultiLineString(line_strings)
+
+ def get_ripple_target(self):
+ command = self.get_command('ripple_target')
+ if command:
+ pos = [float(command.use.get("x", 0)), float(command.use.get("y", 0))]
+ transform = get_node_transform(command.use)
+ pos = Transform(transform).apply_to_point(pos)
+ return Point(*pos)
+ else:
+ return self.shape.centroid
def is_running_stitch(self):
# using stroke width <= 0.5 pixels to indicate running stitch is deprecated in favor of dashed lines
@@ -166,6 +371,10 @@ class Stroke(EmbroideryElement):
for i in range(len(patch) - 1):
start = patch.stitches[i]
end = patch.stitches[i + 1]
+ # sometimes the stitch results into zero length which cause a division by zero error
+ # ignoring this leads to a slightly bad result, but that is better than no output
+ if (end - start).length() == 0:
+ continue
segment_direction = (end - start).unit()
zigzag_direction = segment_direction.rotate_left()
@@ -193,23 +402,65 @@ class Stroke(EmbroideryElement):
return StitchGroup(self.color, stitches)
- def to_stitch_groups(self, last_patch):
- patches = []
+ def ripple_stitch(self):
+ return StitchGroup(
+ color=self.color,
+ tags=["ripple_stitch"],
+ stitches=ripple_stitch(self))
- for path in self.paths:
- path = [Point(x, y) for x, y in path]
- if self.manual_stitch_mode:
- patch = StitchGroup(color=self.color, stitches=path, stitch_as_is=True)
- elif self.is_running_stitch():
- patch = self.running_stitch(path, self.running_stitch_length)
-
- if self.bean_stitch_repeats > 0:
- patch.stitches = bean_stitch(patch.stitches, self.bean_stitch_repeats)
+ def do_bean_repeats(self, stitches):
+ return bean_stitch(stitches, self.bean_stitch_repeats)
- else:
- patch = self.simple_satin(path, self.zigzag_spacing, self.stroke_width)
+ def to_stitch_groups(self, last_patch):
+ patches = []
+ # ripple stitch
+ if self.stroke_method == 1:
+ patch = self.ripple_stitch()
if patch:
+ if self.bean_stitch_repeats > 0:
+ patch.stitches = self.do_bean_repeats(patch.stitches)
patches.append(patch)
+ else:
+ for path in self.paths:
+ path = [Point(x, y) for x, y in path]
+ # manual stitch
+ if self.manual_stitch_mode:
+ patch = StitchGroup(color=self.color, stitches=path, stitch_as_is=True)
+ # running stitch
+ elif self.is_running_stitch():
+ patch = self.running_stitch(path, self.running_stitch_length)
+ if self.bean_stitch_repeats > 0:
+ patch.stitches = self.do_bean_repeats(patch.stitches)
+ # simple satin
+ else:
+ patch = self.simple_satin(path, self.zigzag_spacing, self.stroke_width)
+
+ if patch:
+ patches.append(patch)
return patches
+
+ @cache
+ def get_guide_line(self):
+ guide_lines = get_marker_elements(self.node, "guide-line", False, True, True)
+ # No or empty guide line
+ # if there is a satin guide line, it will also be in stroke, so no need to check for satin here
+ if not guide_lines or not guide_lines['stroke']:
+ return None
+
+ # use the satin guide line if there is one, else use stroke
+ # ignore multiple guide lines
+ if len(guide_lines['satin']) >= 1:
+ return guide_lines['satin'][0]
+ return guide_lines['stroke'][0]
+
+ def validation_warnings(self):
+ if self.stroke_method == 1 and self.skip_start + self.skip_end >= self.line_count:
+ yield IgnoreSkipValues(self.shape.centroid)
+
+ # guided fill warnings
+ if self.stroke_method == 1:
+ guide_lines = get_marker_elements(self.node, "guide-line", False, True, True)
+ if sum(len(x) for x in guide_lines.values()) > 1:
+ yield MultipleGuideLineWarning(self.shape.centroid)
diff --git a/lib/elements/utils.py b/lib/elements/utils.py
index 99df7002..dafde759 100644
--- a/lib/elements/utils.py
+++ b/lib/elements/utils.py
@@ -4,36 +4,36 @@
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
from ..commands import is_command
-from ..patterns import is_pattern
+from ..marker import has_marker
from ..svg.tags import (EMBROIDERABLE_TAGS, SVG_IMAGE_TAG, SVG_PATH_TAG,
SVG_POLYLINE_TAG, SVG_TEXT_TAG)
-from .auto_fill import AutoFill
+from .fill_stitch import FillStitch
from .clone import Clone, is_clone
from .element import EmbroideryElement
from .empty_d_object import EmptyDObject
-from .fill import Fill
from .image import ImageObject
-from .pattern import PatternObject
+from .marker import MarkerObject
from .polyline import Polyline
from .satin_column import SatinColumn
from .stroke import Stroke
from .text import TextObject
-def node_to_elements(node): # noqa: C901
+def node_to_elements(node, clone_to_element=False): # noqa: C901
if node.tag == SVG_POLYLINE_TAG:
return [Polyline(node)]
- elif is_clone(node):
+ elif is_clone(node) and not clone_to_element:
+ # clone_to_element: get an actual embroiderable element once a clone has been defined as a clone
return [Clone(node)]
elif node.tag == SVG_PATH_TAG and not node.get('d', ''):
return [EmptyDObject(node)]
- elif is_pattern(node):
- return [PatternObject(node)]
+ elif has_marker(node):
+ return [MarkerObject(node)]
- elif node.tag in EMBROIDERABLE_TAGS:
+ elif node.tag in EMBROIDERABLE_TAGS or is_clone(node):
element = EmbroideryElement(node)
if element.get_boolean_param("satin_column") and element.get_style("stroke"):
@@ -41,10 +41,7 @@ def node_to_elements(node): # noqa: C901
else:
elements = []
if element.get_style("fill", "black") and not element.get_style('fill-opacity', 1) == "0":
- if element.get_boolean_param("auto_fill", True):
- elements.append(AutoFill(node))
- else:
- elements.append(Fill(node))
+ elements.append(FillStitch(node))
if element.get_style("stroke"):
if not is_command(element.node):
elements.append(Stroke(node))