kopia lustrzana https://github.com/inkstitch/inkstitch
add Troubleshoot extension (#465)
adds an extension to help you understand what's wrong with an object and how to fix it, e.g. "invalid" fill shapespull/509/head
rodzic
cdb8d1d0d4
commit
077f7ea72b
|
@ -371,3 +371,22 @@ def add_commands(element, commands):
|
|||
pos = get_command_pos(element, i, len(commands))
|
||||
symbol = add_symbol(document, group, command, pos)
|
||||
add_connector(document, symbol, element)
|
||||
|
||||
|
||||
def add_layer_commands(layer, commands):
|
||||
document = get_document(layer)
|
||||
correction_transform = get_correction_transform(layer)
|
||||
|
||||
for command in commands:
|
||||
ensure_symbol(document, command)
|
||||
inkex.etree.SubElement(layer, SVG_USE_TAG,
|
||||
{
|
||||
"id": generate_unique_id(document, "use"),
|
||||
INKSCAPE_LABEL: _("Ink/Stitch Command") + ": %s" % get_command_description(command),
|
||||
XLINK_HREF: "#inkstitch_%s" % command,
|
||||
"height": "100%",
|
||||
"width": "100%",
|
||||
"x": "0",
|
||||
"y": "-10",
|
||||
"transform": correction_transform
|
||||
})
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
|
@ -2,6 +2,7 @@ from auto_satin import AutoSatin
|
|||
from convert_to_satin import ConvertToSatin
|
||||
from cut_satin import CutSatin
|
||||
from embroider import Embroider
|
||||
from lib.extensions.troubleshoot import Troubleshoot
|
||||
from flip import Flip
|
||||
from global_commands import GlobalCommands
|
||||
from input import Input
|
||||
|
@ -31,4 +32,5 @@ __all__ = extensions = [Embroider,
|
|||
ConvertToSatin,
|
||||
CutSatin,
|
||||
AutoSatin,
|
||||
Lettering]
|
||||
Lettering,
|
||||
Troubleshoot]
|
||||
|
|
|
@ -0,0 +1,176 @@
|
|||
from itertools import chain
|
||||
import textwrap
|
||||
|
||||
import inkex
|
||||
|
||||
from ..commands import add_layer_commands
|
||||
from ..elements.validation import ValidationWarning, ValidationError
|
||||
from ..i18n import _
|
||||
from ..svg import get_correction_transform
|
||||
from ..svg.tags import (INKSCAPE_GROUPMODE, INKSCAPE_LABEL,
|
||||
SODIPODI_ROLE, SVG_GROUP_TAG, SVG_PATH_TAG,
|
||||
SVG_TEXT_TAG, SVG_TSPAN_TAG)
|
||||
from .base import InkstitchExtension
|
||||
|
||||
|
||||
class Troubleshoot(InkstitchExtension):
|
||||
|
||||
def effect(self):
|
||||
if not self.get_elements():
|
||||
return
|
||||
|
||||
self.create_troubleshoot_layer()
|
||||
|
||||
problem_types = set()
|
||||
for element in self.elements:
|
||||
for problem in chain(element.validation_errors(), element.validation_warnings()):
|
||||
problem_types.add(type(problem))
|
||||
self.insert_pointer(problem)
|
||||
|
||||
if problem_types:
|
||||
self.add_descriptions(problem_types)
|
||||
else:
|
||||
svg = self.document.getroot()
|
||||
svg.remove(self.troubleshoot_layer)
|
||||
|
||||
message = _("All selected shapes are valid!")
|
||||
message += "\n\n"
|
||||
message += _("Tip: If you are still having an issue with an object not being rendered, "
|
||||
"you might need to convert it it to a path (Path -> Object to Path) or check if it is possibly in an ignored layer.")
|
||||
|
||||
inkex.errormsg(message)
|
||||
|
||||
def insert_pointer(self, problem):
|
||||
correction_transform = get_correction_transform(self.troubleshoot_layer)
|
||||
|
||||
if isinstance(problem, ValidationWarning):
|
||||
fill_color = "#ffdd00"
|
||||
layer = self.warning_group
|
||||
elif isinstance(problem, ValidationError):
|
||||
fill_color = "#ff0000"
|
||||
layer = self.error_group
|
||||
|
||||
pointer_style = "stroke:#ffffff;stroke-width:0.2;fill:%s;" % (fill_color)
|
||||
text_style = "fill:%s;stroke:#ffffff;stroke-width:0.2;font-size:8px;text-align:center;text-anchor:middle" % (fill_color)
|
||||
|
||||
path = inkex.etree.Element(
|
||||
SVG_PATH_TAG,
|
||||
{
|
||||
"id": self.uniqueId("inkstitch__invalid_pointer__"),
|
||||
"d": "m %s,%s 4,20 h -8 l 4,-20" % (problem.position.x, problem.position.y),
|
||||
"style": pointer_style,
|
||||
INKSCAPE_LABEL: _('Invalid Pointer'),
|
||||
"transform": correction_transform
|
||||
}
|
||||
)
|
||||
layer.insert(0, path)
|
||||
|
||||
text = inkex.etree.Element(
|
||||
SVG_TEXT_TAG,
|
||||
{
|
||||
INKSCAPE_LABEL: _('Description'),
|
||||
"x": str(problem.position.x),
|
||||
"y": str(float(problem.position.y) + 30),
|
||||
"transform": correction_transform,
|
||||
"style": text_style
|
||||
}
|
||||
)
|
||||
layer.append(text)
|
||||
|
||||
tspan = inkex.etree.Element(SVG_TSPAN_TAG)
|
||||
tspan.text = problem.name
|
||||
text.append(tspan)
|
||||
|
||||
def create_troubleshoot_layer(self):
|
||||
svg = self.document.getroot()
|
||||
layer = svg.find(".//*[@id='__validation_layer__']")
|
||||
|
||||
if layer is None:
|
||||
layer = inkex.etree.Element(
|
||||
SVG_GROUP_TAG,
|
||||
{
|
||||
'id': '__validation_layer__',
|
||||
INKSCAPE_LABEL: _('Troubleshoot'),
|
||||
INKSCAPE_GROUPMODE: 'layer',
|
||||
})
|
||||
svg.append(layer)
|
||||
|
||||
else:
|
||||
# Clear out everything from the last run
|
||||
del layer[:]
|
||||
|
||||
add_layer_commands(layer, ["ignore_layer"])
|
||||
|
||||
error_group = inkex.etree.SubElement(
|
||||
layer,
|
||||
SVG_GROUP_TAG,
|
||||
{
|
||||
"id": '__validation_errors__',
|
||||
INKSCAPE_LABEL: _("Errors"),
|
||||
})
|
||||
layer.append(error_group)
|
||||
|
||||
warning_group = inkex.etree.SubElement(
|
||||
layer,
|
||||
SVG_GROUP_TAG,
|
||||
{
|
||||
"id": '__validation_warnings__',
|
||||
INKSCAPE_LABEL: _("Warnings"),
|
||||
})
|
||||
layer.append(warning_group)
|
||||
|
||||
self.troubleshoot_layer = layer
|
||||
self.error_group = error_group
|
||||
self.warning_group = warning_group
|
||||
|
||||
def add_descriptions(self, problem_types):
|
||||
svg = self.document.getroot()
|
||||
text_x = str(self.unittouu(svg.get('width')) + 5)
|
||||
|
||||
text_container = inkex.etree.Element(
|
||||
SVG_TEXT_TAG,
|
||||
{
|
||||
"x": text_x,
|
||||
"y": str(5),
|
||||
"style": "fill:#000000;font-size:5px;line-height:1;"
|
||||
}
|
||||
)
|
||||
self.troubleshoot_layer.append(text_container)
|
||||
|
||||
text = [
|
||||
[_("Troubleshoot"), "font-weight: bold; font-size: 6px;"],
|
||||
["", ""]
|
||||
]
|
||||
|
||||
for problem in problem_types:
|
||||
text_color = "#ff0000"
|
||||
if issubclass(problem, ValidationWarning):
|
||||
text_color = "#ffdd00"
|
||||
|
||||
text.append([problem.name, "font-weight: bold; fill:%s;" % text_color])
|
||||
description_parts = textwrap.wrap(problem.description, 60)
|
||||
for description in description_parts:
|
||||
text.append([description, "font-size: 3px;"])
|
||||
text.append(["", ""])
|
||||
for step in problem.steps_to_solve:
|
||||
text.append([step, "font-size: 4px;"])
|
||||
text.append(["", ""])
|
||||
|
||||
explain_layer = _('It is possible, that one object contains more than one error, ' +
|
||||
'yet there will be only one pointer per object. Run this function again, ' +
|
||||
'when further errors occur. Remove pointers by deleting the layer named '
|
||||
'"Troubleshoot" through the objects panel (Object -> Objects...).')
|
||||
explain_layer_parts = textwrap.wrap(explain_layer, 60)
|
||||
for description in explain_layer_parts:
|
||||
text.append([description, "font-style: italic; font-size: 3px;"])
|
||||
|
||||
for text_line in text:
|
||||
tspan = inkex.etree.Element(
|
||||
SVG_TSPAN_TAG,
|
||||
{
|
||||
SODIPODI_ROLE: "line",
|
||||
"style": text_line[1]
|
||||
}
|
||||
)
|
||||
tspan.text = text_line[0]
|
||||
text_container.append(tspan)
|
|
@ -9,18 +9,12 @@ from shapely.ops import snap
|
|||
from shapely.strtree import STRtree
|
||||
|
||||
from ..debug import debug
|
||||
from ..exceptions import InkstitchException
|
||||
from ..i18n import _
|
||||
from ..svg import PIXELS_PER_MM
|
||||
from ..utils.geometry import Point as InkstitchPoint
|
||||
from ..utils.geometry import Point as InkstitchPoint, line_string_to_point_list
|
||||
from .fill import intersect_region_with_grating, stitch_row
|
||||
from .running_stitch import running_stitch
|
||||
|
||||
|
||||
class InvalidPath(InkstitchException):
|
||||
pass
|
||||
|
||||
|
||||
class PathEdge(object):
|
||||
OUTLINE_KEYS = ("outline", "extra", "initial")
|
||||
SEGMENT_KEY = "segment"
|
||||
|
@ -60,7 +54,10 @@ def auto_fill(shape,
|
|||
underpath=True):
|
||||
|
||||
fill_stitch_graph = build_fill_stitch_graph(shape, angle, row_spacing, end_row_spacing)
|
||||
check_graph(fill_stitch_graph, shape, max_stitch_length)
|
||||
|
||||
if not graph_is_valid(fill_stitch_graph, shape, max_stitch_length):
|
||||
return fallback(shape, running_stitch_length)
|
||||
|
||||
travel_graph = build_travel_graph(fill_stitch_graph, shape, angle, underpath)
|
||||
path = find_stitch_path(fill_stitch_graph, travel_graph, starting_point, ending_point)
|
||||
result = path_to_stitches(path, travel_graph, fill_stitch_graph, angle, row_spacing,
|
||||
|
@ -188,6 +185,25 @@ def add_edges_between_outline_nodes(graph, duplicate_every_other=False):
|
|||
graph.add_edge(node1, node2, key="extra", **data)
|
||||
|
||||
|
||||
def graph_is_valid(graph, shape, max_stitch_length):
|
||||
# The graph may be empty if the shape is so small that it fits between the
|
||||
# rows of stitching. Certain small weird shapes can also cause a non-
|
||||
# eulerian graph.
|
||||
return not networkx.is_empty(graph) and networkx.is_eulerian(graph)
|
||||
|
||||
|
||||
def fallback(shape, running_stitch_length):
|
||||
"""Generate stitches when the auto-fill algorithm fails.
|
||||
|
||||
If graph_is_valid() returns False, we're not going to be able to run the
|
||||
auto-fill algorithm. Instead, we'll just do running stitch around the
|
||||
outside of the shape. In all likelihood, the shape is so small it won't
|
||||
matter.
|
||||
"""
|
||||
|
||||
return running_stitch(line_string_to_point_list(shape.boundary[0]), running_stitch_length)
|
||||
|
||||
|
||||
@debug.time
|
||||
def build_travel_graph(fill_stitch_graph, shape, fill_stitch_angle, underpath):
|
||||
"""Build a graph for travel stitches.
|
||||
|
@ -376,18 +392,6 @@ def build_travel_edges(shape, fill_angle):
|
|||
return endpoints, chain(diagonal_edges, vertical_edges)
|
||||
|
||||
|
||||
def check_graph(graph, shape, max_stitch_length):
|
||||
if networkx.is_empty(graph) or not networkx.is_eulerian(graph):
|
||||
if shape.area < max_stitch_length ** 2:
|
||||
message = "This shape is so small that it cannot be filled with rows of stitches. " \
|
||||
"It would probably look best as a satin column or running stitch."
|
||||
raise InvalidPath(_(message))
|
||||
else:
|
||||
message = "Cannot parse shape. " \
|
||||
"This most often happens because your shape is made up of multiple sections that aren't connected."
|
||||
raise InvalidPath(_(message))
|
||||
|
||||
|
||||
def nearest_node(nodes, point, attr=None):
|
||||
point = shgeo.Point(*point)
|
||||
nearest = min(nodes, key=lambda node: shgeo.Point(*node).distance(point))
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
from copy import copy
|
||||
|
||||
|
||||
""" Utility functions to produce running stitches. """
|
||||
|
||||
|
||||
|
|
|
@ -6,6 +6,8 @@ inkex.NSS['inkstitch'] = 'http://inkstitch.org/namespace'
|
|||
|
||||
SVG_PATH_TAG = inkex.addNS('path', 'svg')
|
||||
SVG_POLYLINE_TAG = inkex.addNS('polyline', 'svg')
|
||||
SVG_TEXT_TAG = inkex.addNS('text', 'svg')
|
||||
SVG_TSPAN_TAG = inkex.addNS('tspan', 'svg')
|
||||
SVG_DEFS_TAG = inkex.addNS('defs', 'svg')
|
||||
SVG_GROUP_TAG = inkex.addNS('g', 'svg')
|
||||
SVG_SYMBOL_TAG = inkex.addNS('symbol', 'svg')
|
||||
|
@ -19,6 +21,7 @@ CONNECTOR_TYPE = inkex.addNS('connector-type', 'inkscape')
|
|||
XLINK_HREF = inkex.addNS('href', 'xlink')
|
||||
SODIPODI_NAMEDVIEW = inkex.addNS('namedview', 'sodipodi')
|
||||
SODIPODI_GUIDE = inkex.addNS('guide', 'sodipodi')
|
||||
SODIPODI_ROLE = inkex.addNS('role', 'sodipodi')
|
||||
INKSTITCH_LETTERING = inkex.addNS('lettering', 'inkstitch')
|
||||
|
||||
EMBROIDERABLE_TAGS = (SVG_PATH_TAG, SVG_POLYLINE_TAG)
|
||||
|
|
|
@ -128,3 +128,7 @@ class Point:
|
|||
|
||||
def __len__(self):
|
||||
return 2
|
||||
|
||||
|
||||
def line_string_to_point_list(line_string):
|
||||
return [Point(*point) for point in line_string.coords]
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
|
||||
<name>{% trans %}Troubleshoot Objects{% endtrans %}</name>
|
||||
<id>org.inkstitch.troubleshoot.{{ locale }}</id>
|
||||
<dependency type="executable" location="extensions">inkstitch.py</dependency>
|
||||
<dependency type="executable" location="extensions">inkex.py</dependency>
|
||||
<param name="extension" type="string" gui-hidden="true">troubleshoot</param>
|
||||
<effect>
|
||||
<object-type>all</object-type>
|
||||
<effects-menu>
|
||||
<submenu name="Ink/Stitch">
|
||||
</submenu>
|
||||
</effects-menu>
|
||||
</effect>
|
||||
<script>
|
||||
<command reldir="extensions" interpreter="python">inkstitch.py</command>
|
||||
</script>
|
||||
</inkscape-extension>
|
Ładowanie…
Reference in New Issue