summaryrefslogtreecommitdiff
path: root/lib/elements
diff options
context:
space:
mode:
Diffstat (limited to 'lib/elements')
-rw-r--r--lib/elements/auto_fill.py24
-rw-r--r--lib/elements/element.py37
-rw-r--r--lib/elements/fill.py54
-rw-r--r--lib/elements/polyline.py21
-rw-r--r--lib/elements/satin_column.py121
-rw-r--r--lib/elements/stroke.py7
-rw-r--r--lib/elements/validation.py41
7 files changed, 240 insertions, 65 deletions
diff --git a/lib/elements/auto_fill.py b/lib/elements/auto_fill.py
index 62d3493c..a37078b8 100644
--- a/lib/elements/auto_fill.py
+++ b/lib/elements/auto_fill.py
@@ -4,13 +4,21 @@ import traceback
from shapely import geometry as shgeo
-from ..exceptions import InkstitchException
from ..i18n import _
from ..stitches import auto_fill
from ..utils import cache
-from .element import param, Patch
+from .element import Patch, param
from .fill import Fill
+from .validation import 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 AutoFill(Fill):
element_name = _("AutoFill")
@@ -208,10 +216,7 @@ class AutoFill(Fill):
starting_point,
ending_point,
self.underpath))
- except InkstitchException, exc:
- # for one of our exceptions, just print the message
- self.fatal(_("Unable to autofill: ") + str(exc))
- except Exception, exc:
+ except Exception:
if hasattr(sys, 'gettrace') and sys.gettrace():
# if we're debugging, let the exception bubble up
raise
@@ -228,3 +233,10 @@ class AutoFill(Fill):
self.fatal(message)
return [Patch(stitches=stitches, color=self.color)]
+
+ def validation_warnings(self):
+ if self.shape.area < 20:
+ yield SmallShapeWarning(self.shape.centroid)
+
+ for warning in super(AutoFill, self).validation_warnings():
+ yield warning
diff --git a/lib/elements/element.py b/lib/elements/element.py
index e85657cd..dd6c9063 100644
--- a/lib/elements/element.py
+++ b/lib/elements/element.py
@@ -7,7 +7,7 @@ import simplestyle
from ..commands import find_commands
from ..i18n import _
-from ..svg import PIXELS_PER_MM, convert_length, get_doc_size, apply_transforms
+from ..svg import PIXELS_PER_MM, apply_transforms, convert_length, get_doc_size
from ..svg.tags import INKSCAPE_LABEL
from ..utils import cache
@@ -265,6 +265,8 @@ class EmbroideryElement(object):
raise NotImplementedError("%s must implement to_patches()" % self.__class__.__name__)
def embroider(self, last_patch):
+ self.validate()
+
patches = self.to_patches(last_patch)
if patches:
@@ -286,3 +288,36 @@ class EmbroideryElement(object):
error_msg = "%s: %s %s" % (name, _("error:"), message)
print >> sys.stderr, "%s" % (error_msg.encode("UTF-8"))
sys.exit(1)
+
+ def validation_errors(self):
+ """Return a list of errors with this Element.
+
+ Validation errors will prevent the Element from being stitched.
+
+ Return value: an iterable or generator of instances of subclasses of ValidationError
+ """
+ return []
+
+ def validation_warnings(self):
+ """Return a list of warnings about this Element.
+
+ Validation warnings don't prevent the Element from being stitched but
+ the user should probably fix them anyway.
+
+ Return value: an iterable or generator of instances of subclasses of ValidationWarning
+ """
+ return []
+
+ def is_valid(self):
+ # We have to iterate since it could be a generator.
+ for error in self.validation_errors():
+ return False
+
+ return True
+
+ def validate(self):
+ """Print an error message and exit if this Element is invalid."""
+
+ for error in self.validation_errors():
+ # note that self.fatal() exits, so this only shows the first error
+ self.fatal(error.description)
diff --git a/lib/elements/fill.py b/lib/elements/fill.py
index 7ccf7b27..7157cc46 100644
--- a/lib/elements/fill.py
+++ b/lib/elements/fill.py
@@ -1,4 +1,6 @@
+import logging
import math
+import re
from shapely import geometry as shgeo
from shapely.validation import explain_validity
@@ -7,7 +9,29 @@ from ..i18n import _
from ..stitches import legacy_fill
from ..svg import PIXELS_PER_MM
from ..utils import cache
-from .element import param, EmbroideryElement, Patch
+from .element import EmbroideryElement, Patch, param
+from .validation import ValidationError
+
+
+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 = [
+ _('* Path > Break apart (Shift+Ctrl+K)'),
+ _('* (Optional) Recombine shapes with holes (Ctrl+K).')
+ ]
+
+
+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 = [
+ _('* Path > Union (Ctrl++)'),
+ _('* Path > Break apart (Shift+Ctrl+K)'),
+ _('* (Optional) Recombine shapes with holes (Ctrl+K).')
+ ]
class Fill(EmbroideryElement):
@@ -112,18 +136,28 @@ class Fill(EmbroideryElement):
paths.sort(key=lambda point_list: shgeo.Polygon(point_list).area, reverse=True)
polygon = shgeo.MultiPolygon([(paths[0], paths[1:])])
- if not polygon.is_valid:
- why = explain_validity(polygon)
+ return polygon
+
+ def validation_errors(self):
+ # 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 = self.shape.is_valid
+
+ logger.setLevel(level)
+
+ if not valid:
+ 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 why:
- self.fatal(_("this object is made up of unconnected shapes. This is not allowed because "
- "Ink/Stitch doesn't know what order to stitch them in. Please break this "
- "object up into separate shapes."))
+ if "Hole lies outside shell" in message:
+ yield UnconnectedError((x, y))
else:
- self.fatal(_("shape is not valid. This can happen if the border crosses over itself."))
-
- return polygon
+ yield InvalidShapeError((x, y))
def to_patches(self, last_patch):
stitch_lists = legacy_fill(self.shape,
diff --git a/lib/elements/polyline.py b/lib/elements/polyline.py
index 5bfe5022..a9870172 100644
--- a/lib/elements/polyline.py
+++ b/lib/elements/polyline.py
@@ -1,8 +1,22 @@
from shapely import geometry as shgeo
-from .element import EmbroideryElement, Patch
-from ..utils.geometry import Point
+from ..i18n import _
from ..utils import cache
+from ..utils.geometry import Point
+from .element import EmbroideryElement, Patch
+from .validation import ValidationWarning
+
+
+class PolylineWarning(ValidationWarning):
+ name = _("Object is a PolyLine")
+ description = _("This object is an SVG PolyLine. Ink/Stitch can work with this shape, "
+ "but you can't edit it in Inkscape. Convert it to a manual stitch path "
+ "to allow editing.")
+ steps_to_solve = [
+ _("* Select this object."),
+ _("* Do Path > Object to Path."),
+ _('* Optional: Run the Params extension and check the "manual stitch" box.')
+ ]
class Polyline(EmbroideryElement):
@@ -70,6 +84,9 @@ class Polyline(EmbroideryElement):
return stitches
+ def validation_warnings(self):
+ yield PolylineWarning(self.points[0])
+
def to_patches(self, last_patch):
patch = Patch(color=self.color)
diff --git a/lib/elements/satin_column.py b/lib/elements/satin_column.py
index d3c4d3d3..ccad234a 100644
--- a/lib/elements/satin_column.py
+++ b/lib/elements/satin_column.py
@@ -7,7 +7,54 @@ from shapely import geometry as shgeo, affinity as shaffinity
from ..i18n import _
from ..svg import line_strings_to_csp, point_lists_to_csp
from ..utils import cache, Point, cut, collapse_duplicate_point
+
from .element import param, EmbroideryElement, Patch
+from .validation import ValidationError
+
+
+class SatinHasFillError(ValidationError):
+ name = _("Satin column has fill")
+ description = _("Satin column: Object has a fill (but should not)")
+ steps_to_solve = [
+ _("* Select this object."),
+ _("* Open the Fill and Stroke panel"),
+ _("* Open the Fill tab"),
+ _("* Disable the Fill"),
+ _("* Alternative: open Params and switch this path to Stroke to disable Satin Column mode")
+ ]
+
+
+class TooFewPathsError(ValidationError):
+ name = _("Too few subpaths")
+ description = _("Satin column: Object has too few subpaths. A satin column should have at least two subpaths (the rails).")
+ steps_to_solve = [
+ _("* Add another subpath (select two rails and do Path > Combine)"),
+ _("* Convert to running stitch or simple satin (Params extension)")
+ ]
+
+
+class UnequalPointsError(ValidationError):
+ name = _("Unequal number of points")
+ description = _("Satin column: There are no rungs and rails have an an unequal number of points.")
+ steps_to_solve = [
+ _('The easiest way to solve this issue is to add one or more rungs. '),
+ _('Rungs control the stitch direction in satin columns.'),
+ _('* With the selected object press "P" to activate the pencil tool.'),
+ _('* Hold "Shift" while drawing the rung.')
+ ]
+
+
+rung_message = _("Each rung should intersect both rails once.")
+
+
+class DanglingRungError(ValidationError):
+ name = _("Rung doesn't intersect rails")
+ description = _("Satin column: A rung doesn't intersect both rails.") + " " + rung_message
+
+
+class TooManyIntersectionsError(ValidationError):
+ name = _("Rung intersects too many times")
+ description = _("Satin column: A rung intersects a rail more than once.") + " " + rung_message
class SatinColumn(EmbroideryElement):
@@ -146,13 +193,25 @@ class SatinColumn(EmbroideryElement):
@property
@cache
def rails(self):
- """The rails in order, as LineStrings"""
+ """The rails in order, as point lists"""
return [subpath for i, subpath in enumerate(self.csp) if i in self.rail_indices]
@property
@cache
+ def flattened_rails(self):
+ """The rails, as LineStrings."""
+ return tuple(shgeo.LineString(self.flatten_subpath(rail)) for rail in self.rails)
+
+ @property
+ @cache
+ def flattened_rungs(self):
+ """The rungs, as LineStrings."""
+ return tuple(shgeo.LineString(self.flatten_subpath(rung)) for rung in self.rungs)
+
+ @property
+ @cache
def rungs(self):
- """The rungs, as LineStrings.
+ """The rungs, as point lists.
If there are no rungs, then this is an old-style satin column. The
rails are expected to have the same number of path nodes. The path
@@ -238,8 +297,6 @@ class SatinColumn(EmbroideryElement):
return indices_by_length[:2]
def _cut_rail(self, rail, rung):
- intersections = 0
-
for segment_index, rail_segment in enumerate(rail[:]):
if rail_segment is None:
continue
@@ -252,14 +309,6 @@ class SatinColumn(EmbroideryElement):
intersection = collapse_duplicate_point(intersection)
if not intersection.is_empty:
- if isinstance(intersection, shgeo.MultiLineString):
- intersections += len(intersection)
- break
- elif not isinstance(intersection, shgeo.Point):
- self.fatal("INTERNAL ERROR: intersection is: %s %s" % (intersection, getattr(intersection, 'geoms', None)))
- else:
- intersections += 1
-
cut_result = cut(rail_segment, rail_segment.project(intersection))
rail[segment_index:segment_index + 1] = cut_result
@@ -269,29 +318,17 @@ class SatinColumn(EmbroideryElement):
# segment
break
- return intersections
-
@property
@cache
def flattened_sections(self):
"""Flatten the rails, cut with the rungs, and return the sections in pairs."""
- if len(self.csp) < 2:
- self.fatal(_("satin column: %(id)s: at least two subpaths required (%(num)d found)") % dict(num=len(self.csp), id=self.node.get('id')))
-
- rails = [[shgeo.LineString(self.flatten_subpath(rail))] for rail in self.rails]
- rungs = [shgeo.LineString(self.flatten_subpath(rung)) for rung in self.rungs]
+ rails = [[rail] for rail in self.flattened_rails]
+ rungs = self.flattened_rungs
for rung in rungs:
- for rail_index, rail in enumerate(rails):
- intersections = self._cut_rail(rail, rung)
-
- if intersections == 0:
- self.fatal(_("satin column: One or more of the rungs doesn't intersect both rails.") +
- " " + _("Each rail should intersect both rungs once."))
- elif intersections > 1:
- self.fatal(_("satin column: One or more of the rungs intersects the rails more than once.") +
- " " + _("Each rail should intersect both rungs once."))
+ for rail in rails:
+ self._cut_rail(rail, rung)
for rail in rails:
for i in xrange(len(rail)):
@@ -314,24 +351,27 @@ class SatinColumn(EmbroideryElement):
return sections
- def validate_satin_column(self):
+ def validation_errors(self):
# The node should have exactly two paths with no fill. Each
# path should have the same number of points, meaning that they
# will both be made up of the same number of bezier curves.
- node_id = self.node.get("id")
-
if self.get_style("fill") is not None:
- self.fatal(_("satin column: object %s has a fill (but should not)") % node_id)
-
- if not self.rungs:
- if len(self.rails) < 2:
- self.fatal(_("satin column: object %(id)s has too few paths. A satin column should have at least two paths (the rails).") %
- dict(id=node_id))
+ yield SatinHasFillError(self.shape.centroid)
+ if len(self.rails) < 2:
+ yield TooFewPathsError(self.shape.centroid)
+ elif len(self.csp) == 2:
if len(self.rails[0]) != len(self.rails[1]):
- self.fatal(_("satin column: object %(id)s has two paths with an unequal number of points (%(length1)d and %(length2)d)") %
- dict(id=node_id, length1=len(self.rails[0]), length2=len(self.rails[1])))
+ yield UnequalPointsError(self.flattened_rails[0].interpolate(0.5, normalized=True))
+ else:
+ for rung in self.flattened_rungs:
+ for rail in self.flattened_rails:
+ intersection = rung.intersection(rail)
+ if intersection.is_empty:
+ yield DanglingRungError(rung.interpolate(0.5, normalized=True))
+ elif not isinstance(intersection, shgeo.Point):
+ yield TooManyIntersectionsError(rung.interpolate(0.5, normalized=True))
def reverse(self):
"""Return a new SatinColumn like this one but in the opposite direction.
@@ -772,9 +812,6 @@ class SatinColumn(EmbroideryElement):
# beziers. The boundary points between beziers serve as "checkpoints",
# allowing the user to control how the zigzags flow around corners.
- # First, verify that we have valid paths.
- self.validate_satin_column()
-
patch = Patch(color=self.color)
if self.center_walk_underlay:
diff --git a/lib/elements/stroke.py b/lib/elements/stroke.py
index e0a0aacc..4828bf65 100644
--- a/lib/elements/stroke.py
+++ b/lib/elements/stroke.py
@@ -3,11 +3,10 @@ import sys
import shapely.geometry
from ..i18n import _
-from ..stitches import running_stitch, bean_stitch
+from ..stitches import bean_stitch, running_stitch
from ..svg import parse_length_with_units
-from ..utils import cache, Point
-from .element import param, EmbroideryElement, Patch
-
+from ..utils import Point, cache
+from .element import EmbroideryElement, Patch, param
warned_about_legacy_running_stitch = False
diff --git a/lib/elements/validation.py b/lib/elements/validation.py
new file mode 100644
index 00000000..41098922
--- /dev/null
+++ b/lib/elements/validation.py
@@ -0,0 +1,41 @@
+from shapely.geometry import Point as ShapelyPoint
+
+from ..utils import Point as InkstitchPoint
+
+
+class ValidationMessage(object):
+ '''Holds information about a problem with an element.
+
+ Attributes:
+ name - A short descriptor for the problem, such as "dangling rung"
+ description - A detailed description of the problem, such as
+ "One or more rungs does not intersect both rails."
+ position - An optional position where the problem occurs,
+ to aid the user in correcting it. type: Point or tuple of (x, y)
+ steps_to_solve - A list of operations necessary to solve the problem
+ '''
+
+ # Subclasses will fill these in.
+ name = None
+ description = None
+ steps_to_solve = []
+
+ def __init__(self, position=None):
+ if isinstance(position, ShapelyPoint):
+ position = (position.x, position.y)
+
+ self.position = InkstitchPoint(*position)
+
+
+class ValidationError(ValidationMessage):
+ """A problem that will prevent the shape from being embroidered."""
+ pass
+
+
+class ValidationWarning(ValidationMessage):
+ """A problem that won't prevent a shape from being embroidered.
+
+ The user will almost certainly want to fix the warning, but if they
+ don't, Ink/Stitch will do its best to process the object.
+ """
+ pass