summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/commands.py14
-rw-r--r--lib/elements/__init__.py3
-rw-r--r--lib/elements/clone.py169
-rw-r--r--lib/elements/element.py22
-rw-r--r--lib/elements/fill.py12
-rw-r--r--lib/elements/image.py34
-rw-r--r--lib/elements/polyline.py13
-rw-r--r--lib/elements/svg_objects.py71
-rw-r--r--lib/elements/text.py32
-rw-r--r--lib/elements/utils.py29
-rw-r--r--lib/elements/validation.py10
-rw-r--r--lib/extensions/base.py44
-rw-r--r--lib/extensions/params.py32
-rw-r--r--lib/extensions/remove_embroidery_settings.py20
-rw-r--r--lib/extensions/simulator.py2
-rw-r--r--lib/extensions/troubleshoot.py118
-rw-r--r--lib/svg/path.py3
-rw-r--r--lib/svg/svg.py8
-rw-r--r--lib/svg/tags.py11
19 files changed, 545 insertions, 102 deletions
diff --git a/lib/commands.py b/lib/commands.py
index b92d79cf..9d0b243c 100644
--- a/lib/commands.py
+++ b/lib/commands.py
@@ -240,6 +240,14 @@ def is_command(node):
return CONNECTION_START in node.attrib or CONNECTION_END in node.attrib
+def is_command_symbol(node):
+ symbol = None
+ xlink = node.get(XLINK_HREF, "")
+ if xlink.startswith("#inkstitch_"):
+ symbol = node.get(XLINK_HREF)[11:]
+ return symbol in COMMANDS
+
+
@cache
def symbols_path():
return os.path.join(get_bundled_dir("symbols"), "inkstitch.svg")
@@ -280,7 +288,7 @@ def add_group(document, node, command):
node.getparent(),
SVG_GROUP_TAG,
{
- "id": generate_unique_id(document, "group"),
+ "id": generate_unique_id(document, "command_group"),
INKSCAPE_LABEL: _("Ink/Stitch Command") + ": %s" % get_command_description(command),
"transform": get_correction_transform(node)
})
@@ -298,7 +306,7 @@ def add_connector(document, symbol, element):
path = inkex.etree.Element(SVG_PATH_TAG,
{
- "id": generate_unique_id(document, "connector"),
+ "id": generate_unique_id(document, "command_connector"),
"d": "M %s,%s %s,%s" % (start_pos[0], start_pos[1], end_pos.x, end_pos.y),
"style": "stroke:#000000;stroke-width:1px;stroke-opacity:0.5;fill:none;",
CONNECTION_START: "#%s" % symbol.get('id'),
@@ -315,7 +323,7 @@ def add_connector(document, symbol, element):
def add_symbol(document, group, command, pos):
return inkex.etree.SubElement(group, SVG_USE_TAG,
{
- "id": generate_unique_id(document, "use"),
+ "id": generate_unique_id(document, "command_use"),
XLINK_HREF: "#inkstitch_%s" % command,
"height": "100%",
"width": "100%",
diff --git a/lib/elements/__init__.py b/lib/elements/__init__.py
index 5413ba04..75509e29 100644
--- a/lib/elements/__init__.py
+++ b/lib/elements/__init__.py
@@ -1,7 +1,10 @@
from auto_fill import AutoFill
+from clone import Clone
from element import EmbroideryElement
from fill import Fill
+from image import ImageObject
from polyline import Polyline
from satin_column import SatinColumn
from stroke import Stroke
+from text import TextObject
from utils import node_to_elements, nodes_to_elements
diff --git a/lib/elements/clone.py b/lib/elements/clone.py
new file mode 100644
index 00000000..0e7a5d63
--- /dev/null
+++ b/lib/elements/clone.py
@@ -0,0 +1,169 @@
+from copy import copy
+from math import atan, degrees
+
+from simpletransform import (applyTransformToNode, applyTransformToPoint,
+ computeBBox, parseTransform)
+
+from ..commands import is_command, 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 ..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
+
+
+class CloneWarning(ValidationWarning):
+ name = _("Clone Object")
+ description = _("There are one or more clone objects in this document. "
+ "Ink/Stitch can work with single clones, but you are limited to set a very few parameters. ")
+ steps_to_solve = [
+ _("If you want to convert the clone into a real element, follow these steps:"),
+ _("* Select the clone"),
+ _("* Run: Edit > Clone > Unlink Clone (Alt+Shift+D)")
+ ]
+
+
+class CloneSourceWarning(ObjectTypeWarning):
+ name = _("Clone is not embroiderable")
+ description = _("There are one ore more clone objects in this document. A clone must be a direct child of an embroiderable element. "
+ "Ink/Stitch cannot embroider clones of groups or other not embroiderable elements (text or image).")
+ steps_to_solve = [
+ _("Convert the clone into a real element:"),
+ _("* Select the clone."),
+ _("* Run: Edit > Clone > Unlink Clone (Alt+Shift+D)")
+ ]
+
+
+class Clone(EmbroideryElement):
+ # A clone embroidery element is linked to an embroiderable element.
+ # It will be ignored if the source element is not a direct child of the xlink attribute.
+
+ element_name = "Clone"
+
+ def __init__(self, *args, **kwargs):
+ super(Clone, self).__init__(*args, **kwargs)
+
+ @property
+ @param('clone', _("Clone"), type='toggle', inverse=False, default=True)
+ def clone(self):
+ return self.get_boolean_param("clone")
+
+ @property
+ @param('angle',
+ _('Custom fill angle'),
+ tooltip=_("This setting will apply a custom fill angle for the clone."),
+ unit='deg',
+ type='float')
+ @cache
+ def clone_fill_angle(self):
+ 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 element.get_style("stroke"):
+ return [SatinColumn(node)]
+ 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))
+ if element.get_style("stroke"):
+ if not is_command(element.node):
+ elements.append(Stroke(node))
+ if element.get_boolean_param("stroke_first", False):
+ elements.reverse()
+
+ return elements
+
+ def to_patches(self, last_patch=None):
+ patches = []
+
+ source_node = get_clone_source(self.node)
+ if source_node.tag not in EMBROIDERABLE_TAGS:
+ return []
+
+ clone = copy(source_node)
+
+ # set id
+ clone_id = 'clone__%s__%s' % (self.node.get('id', ''), clone.get('id', ''))
+ clone.set('id', clone_id)
+
+ # apply transform
+ transform = get_node_transform(self.node)
+ applyTransformToNode(transform, clone)
+
+ # set fill angle. Use either
+ # a. a custom set fill angle
+ # b. calculated rotation for the cloned fill element to look exactly as it's source
+ param = INKSTITCH_ATTRIBS['angle']
+ if self.clone_fill_angle is not None:
+ angle = self.clone_fill_angle
+ else:
+ # clone angle
+ clone_mat = parseTransform(clone.get('transform', ''))
+ clone_angle = degrees(atan(-clone_mat[1][0]/clone_mat[1][1]))
+ # source node angle
+ source_mat = parseTransform(source_node.get('transform', ''))
+ source_angle = degrees(atan(-source_mat[1][0]/source_mat[1][1]))
+ # source node fill angle
+ source_fill_angle = source_node.get(param, 0)
+
+ angle = clone_angle + float(source_fill_angle) - source_angle
+ clone.set(param, str(angle))
+
+ elements = self.clone_to_element(clone)
+
+ for element in elements:
+ patches.extend(element.to_patches(last_patch))
+
+ return patches
+
+ def center(self, source_node):
+ xmin, xmax, ymin, ymax = computeBBox([source_node])
+ point = [(xmax-((xmax-xmin)/2)), (ymax-((ymax-ymin)/2))]
+ transform = get_node_transform(self.node)
+ applyTransformToPoint(transform, point)
+ return point
+
+ def validation_warnings(self):
+ source_node = get_clone_source(self.node)
+ if source_node.tag not in EMBROIDERABLE_TAGS:
+ point = self.center(source_node)
+ yield CloneSourceWarning(point)
+ else:
+ point = self.center(source_node)
+ yield CloneWarning(point)
+
+
+def is_clone(node):
+ if node.tag == SVG_USE_TAG and node.get(XLINK_HREF) and not is_command_symbol(node):
+ return True
+ return False
+
+
+def is_embroiderable_clone(node):
+ if is_clone(node) and get_clone_source(node).tag in EMBROIDERABLE_TAGS:
+ return True
+ return False
+
+
+def get_clone_source(node):
+ source_id = node.get(XLINK_HREF)[1:]
+ xpath = ".//*[@id='%s']" % (source_id)
+ source_node = find_elements(node, xpath)[0]
+ return source_node
diff --git a/lib/elements/element.py b/lib/elements/element.py
index 62f600d6..f5f774f0 100644
--- a/lib/elements/element.py
+++ b/lib/elements/element.py
@@ -1,15 +1,18 @@
import sys
from copy import deepcopy
-import cubicsuperpath
import tinycss2
+
+import cubicsuperpath
from cspsubdiv import cspsubdiv
from ..commands import find_commands
from ..i18n import _
from ..svg import PIXELS_PER_MM, apply_transforms, convert_length, get_doc_size
-from ..svg.tags import INKSCAPE_LABEL, INKSTITCH_ATTRIBS
+from ..svg.tags import (INKSCAPE_LABEL, INKSTITCH_ATTRIBS, SVG_CIRCLE_TAG,
+ SVG_ELLIPSE_TAG, SVG_OBJECT_TAGS, SVG_RECT_TAG)
from ..utils import cache
+from .svg_objects import circle_to_path, ellipse_to_path, rect_to_path
class Patch:
@@ -181,6 +184,10 @@ class EmbroideryElement(object):
def stroke_scale(self):
svg = self.node.getroottree().getroot()
doc_width, doc_height = get_doc_size(svg)
+ # this is necessary for clones, since they are disconnected from the DOM
+ # it will result in a slighty wrong result for zig-zag stitches
+ if doc_width == 0:
+ return 1
viewbox = svg.get('viewBox', '0 0 %s %s' % (doc_width, doc_height))
viewbox = viewbox.strip().replace(',', ' ').split()
return doc_width / float(viewbox[2])
@@ -236,7 +243,16 @@ class EmbroideryElement(object):
# In a path, each element in the 3-tuple is itself a tuple of (x, y).
# Tuples all the way down. Hasn't anyone heard of using classes?
- d = self.node.get("d", "")
+ if self.node.tag in SVG_OBJECT_TAGS:
+ if self.node.tag == SVG_RECT_TAG:
+ d = rect_to_path(self.node)
+ elif self.node.tag == SVG_ELLIPSE_TAG:
+ d = ellipse_to_path(self.node)
+ elif self.node.tag == SVG_CIRCLE_TAG:
+ d = circle_to_path(self.node)
+ else:
+ d = self.node.get("d", "")
+
if not d:
self.fatal(_("Object %(id)s has an empty 'd' attribute. Please delete this object from your document.") % dict(id=self.node.get("id")))
diff --git a/lib/elements/fill.py b/lib/elements/fill.py
index 0f72d000..59b7414b 100644
--- a/lib/elements/fill.py
+++ b/lib/elements/fill.py
@@ -27,9 +27,15 @@ 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).')
+ _("1. Inkscape has a limit to how far it lets you zoom in. Sometimes there can be a little loop, "
+ "that's so small, you can't see it, but Ink/Stitch can. It's especially common for Inkscape's "
+ "Trace Bitmap to produce those tiny loops."),
+ _("* Delete the node"),
+ _("* Or try to adjust it's handles"),
+ _("2. If you can actually see a loop, run the following commands to seperate the crossing shapes:"),
+ _("* Path > Union (Ctrl++)"),
+ _("* Path > Break apart (Shift+Ctrl+K)"),
+ _("* (Optional) Recombine shapes with holes (Ctrl+K).")
]
diff --git a/lib/elements/image.py b/lib/elements/image.py
new file mode 100644
index 00000000..ec8d1765
--- /dev/null
+++ b/lib/elements/image.py
@@ -0,0 +1,34 @@
+from simpletransform import applyTransformToPoint
+
+from ..i18n import _
+from ..svg import get_node_transform
+from .element import EmbroideryElement
+from .validation import ObjectTypeWarning
+
+
+class ImageTypeWarning(ObjectTypeWarning):
+ name = _("Image")
+ description = _("Ink/Stitch can't work with objects like images.")
+ steps_to_solve = [
+ _('* Convert your image into a path: Path > Trace Bitmap... (Shift+Alt+B) '
+ '(further steps might be required)'),
+ _('* Alternatively redraw the image with the pen (P) or bezier (B) tool')
+ ]
+
+
+class ImageObject(EmbroideryElement):
+
+ def center(self):
+ point = [float(self.node.get('x', 0)), float(self.node.get('y', 0))]
+ point = [(point[0]+(float(self.node.get('width', 0))/2)), (point[1]+(float(self.node.get('height', 0))/2))]
+
+ transform = get_node_transform(self.node)
+ applyTransformToPoint(transform, point)
+
+ return point
+
+ def validation_warnings(self):
+ yield ImageTypeWarning(self.center())
+
+ def to_patches(self, last_patch):
+ return []
diff --git a/lib/elements/polyline.py b/lib/elements/polyline.py
index a9870172..2d008d35 100644
--- a/lib/elements/polyline.py
+++ b/lib/elements/polyline.py
@@ -3,12 +3,12 @@ from shapely import geometry as shgeo
from ..i18n import _
from ..utils import cache
from ..utils.geometry import Point
-from .element import EmbroideryElement, Patch
+from .element import EmbroideryElement, Patch, param
from .validation import ValidationWarning
class PolylineWarning(ValidationWarning):
- name = _("Object is a PolyLine")
+ name = _("Polyline Object")
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.")
@@ -32,6 +32,13 @@ class Polyline(EmbroideryElement):
# users use File -> Import to pull in existing designs they may have
# obtained, for example purchased fonts.
+ element_name = "Polyline"
+
+ @property
+ @param('polyline', _('Manual stitch along path'), type='toggle', inverse=True)
+ def satin_column(self):
+ return self.get_boolean_param("polyline")
+
@property
def points(self):
# example: "1,2 0,0 1.5,3 4,2"
@@ -70,7 +77,7 @@ class Polyline(EmbroideryElement):
def color(self):
# EmbroiderModder2 likes to use the `stroke` property directly instead
# of CSS.
- return self.get_style("stroke") or self.node.get("stroke")
+ return self.get_style("stroke", "#000000")
@property
def stitches(self):
diff --git a/lib/elements/svg_objects.py b/lib/elements/svg_objects.py
new file mode 100644
index 00000000..e597f7c1
--- /dev/null
+++ b/lib/elements/svg_objects.py
@@ -0,0 +1,71 @@
+def rect_to_path(node):
+ x = float(node.get('x', '0'))
+ y = float(node.get('y', '0'))
+ width = float(node.get('width', '0'))
+ height = float(node.get('height', '0'))
+ rx = None
+ ry = None
+
+ # rounded corners
+ # the following rules apply for radius calculations:
+ # if rx or ry is missing it has to take the value of the other one
+ # the radius cannot be bigger than half of the corresponding side
+ # (otherwise we receive an invalid path)
+ if node.get('rx') or node.get('ry'):
+ if node.get('rx'):
+ rx = float(node.get('rx', '0'))
+ ry = rx
+ if node.get('ry'):
+ ry = float(node.get('ry', '0'))
+ if not ry:
+ ry = rx
+
+ rx = min(width/2, rx)
+ ry = min(height/2, ry)
+
+ path = 'M %(startx)f,%(y)f ' \
+ 'h %(width)f ' \
+ 'q %(rx)f,0 %(rx)f,%(ry)f ' \
+ 'v %(height)f ' \
+ 'q 0,%(ry)f -%(rx)f,%(ry)f ' \
+ 'h -%(width)f ' \
+ 'q -%(rx)f,0 -%(rx)f,-%(ry)f ' \
+ 'v -%(height)f ' \
+ 'q 0,-%(ry)f %(rx)f,-%(ry)f ' \
+ 'Z' \
+ % dict(startx=x+rx, x=x, y=y, width=width-(2*rx), height=height-(2*ry), rx=rx, ry=ry)
+
+ else:
+ path = "M %f,%f H %f V %f H %f Z" % (x, y, width+x, height+y, x)
+
+ return path
+
+
+def ellipse_to_path(node):
+ rx = float(node.get('rx', "0")) or float(node.get('r', "0"))
+ ry = float(node.get('ry', "0")) or float(node.get('r', "0"))
+ cx = float(node.get('cx'))
+ cy = float(node.get('cy'))
+
+ path = 'M %(cx_r)f,%(cy)f' \
+ 'C %(cx_r)f,%(cy_r)f %(cx)f,%(cy_r)f %(cx)f,%(cy_r)f ' \
+ '%(cxr)f,%(cy_r)f %(cxr)f,%(cy)f %(cxr)f,%(cy)f ' \
+ '%(cxr)f,%(cyr)f %(cx)f,%(cyr)f %(cx)f,%(cyr)f ' \
+ '%(cx_r)f,%(cyr)f %(cx_r)f,%(cy)f %(cx_r)f,%(cy)f ' \
+ 'Z' \
+ % dict(cx=cx, cx_r=cx-rx, cxr=cx+rx, cy=cy, cyr=cy+ry, cy_r=cy-ry)
+
+ return path
+
+
+def circle_to_path(node):
+ cx = float(node.get('cx'))
+ cy = float(node.get('cy'))
+ r = float(node.get('r'))
+
+ path = 'M %(xstart)f, %(cy)f ' \
+ 'a %(r)f,%(r)f 0 1,0 %(rr)f,0 ' \
+ 'a %(r)f,%(r)f 0 1,0 -%(rr)f,0 ' \
+ % dict(xstart=(cx-r), cy=cy, r=r, rr=(r*2))
+
+ return path
diff --git a/lib/elements/text.py b/lib/elements/text.py
new file mode 100644
index 00000000..2d066bb0
--- /dev/null
+++ b/lib/elements/text.py
@@ -0,0 +1,32 @@
+from simpletransform import applyTransformToPoint
+
+from ..i18n import _
+from ..svg import get_node_transform
+from .element import EmbroideryElement
+from .validation import ObjectTypeWarning
+
+
+class TextTypeWarning(ObjectTypeWarning):
+ name = _("Text")
+ description = _("Ink/Stitch cannot work with objects like text.")
+ steps_to_solve = [
+ _('* Text: Create your own letters or try the lettering tool:'),
+ _('- Extensions > Ink/Stitch > Lettering')
+ ]
+
+
+class TextObject(EmbroideryElement):
+
+ def center(self):
+ point = [float(self.node.get('x', 0)), float(self.node.get('y', 0))]
+
+ transform = get_node_transform(self.node)
+ applyTransformToPoint(transform, point)
+
+ return point
+
+ def validation_warnings(self):
+ yield TextTypeWarning(self.center())
+
+ def to_patches(self, last_patch):
+ return []
diff --git a/lib/elements/utils.py b/lib/elements/utils.py
index 5c71de2e..4719a5ff 100644
--- a/lib/elements/utils.py
+++ b/lib/elements/utils.py
@@ -1,40 +1,49 @@
-
from ..commands import is_command
-from ..svg.tags import SVG_POLYLINE_TAG, SVG_PATH_TAG
-
+from ..svg.tags import (EMBROIDERABLE_TAGS, SVG_IMAGE_TAG, SVG_POLYLINE_TAG,
+ SVG_TEXT_TAG)
from .auto_fill import AutoFill
+from .clone import Clone, is_clone
from .element import EmbroideryElement
from .fill import Fill
+from .image import ImageObject
from .polyline import Polyline
from .satin_column import SatinColumn
from .stroke import Stroke
+from .text import TextObject
-def node_to_elements(node):
+def node_to_elements(node): # noqa: C901
if node.tag == SVG_POLYLINE_TAG:
return [Polyline(node)]
- elif node.tag == SVG_PATH_TAG:
+
+ elif is_clone(node):
+ return [Clone(node)]
+
+ elif node.tag in EMBROIDERABLE_TAGS:
element = EmbroideryElement(node)
if element.get_boolean_param("satin_column") and element.get_style("stroke"):
return [SatinColumn(node)]
else:
elements = []
-
- if element.get_style("fill", "black"):
+ 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))
-
if element.get_style("stroke"):
if not is_command(element.node):
elements.append(Stroke(node))
-
if element.get_boolean_param("stroke_first", False):
elements.reverse()
-
return elements
+
+ elif node.tag == SVG_IMAGE_TAG:
+ return [ImageObject(node)]
+
+ elif node.tag == SVG_TEXT_TAG:
+ return [TextObject(node)]
+
else:
return []
diff --git a/lib/elements/validation.py b/lib/elements/validation.py
index 41098922..f77e2fc4 100644
--- a/lib/elements/validation.py
+++ b/lib/elements/validation.py
@@ -39,3 +39,13 @@ class ValidationWarning(ValidationMessage):
don't, Ink/Stitch will do its best to process the object.
"""
pass
+
+
+class ObjectTypeWarning(ValidationMessage):
+ """A shape is not a path and will not be embroidered.
+
+ Ink/Stitch only works with paths and ignores everything else.
+ The user might want the shape to be ignored, but if they
+ don't, they receive information how to change this behaviour.
+ """
+ pass
diff --git a/lib/extensions/base.py b/lib/extensions/base.py
index 440a5413..310dd873 100644
--- a/lib/extensions/base.py
+++ b/lib/extensions/base.py
@@ -1,18 +1,20 @@
-from collections import MutableMapping
-from copy import deepcopy
import json
import os
import re
+from collections import MutableMapping
+from copy import deepcopy
-import inkex
from stringcase import snakecase
-from ..commands import layer_commands
+import inkex
+
+from ..commands import is_command, layer_commands
from ..elements import EmbroideryElement, nodes_to_elements
+from ..elements.clone import is_clone, is_embroiderable_clone
from ..i18n import _
from ..svg import generate_unique_id
-from ..svg.tags import SVG_GROUP_TAG, INKSCAPE_GROUPMODE, SVG_DEFS_TAG, EMBROIDERABLE_TAGS
-
+from ..svg.tags import (CONNECTOR_TYPE, EMBROIDERABLE_TAGS, INKSCAPE_GROUPMODE,
+ NOT_EMBROIDERABLE_TAGS, SVG_DEFS_TAG, SVG_GROUP_TAG)
SVG_METADATA_TAG = inkex.addNS("metadata", "svg")
@@ -128,10 +130,9 @@ class InkstitchExtension(inkex.Effect):
else:
inkex.errormsg(_("There are no objects in the entire document that Ink/Stitch knows how to work with.") + "\n")
- inkex.errormsg(_("Ink/Stitch only knows how to work with paths. It can't work with objects like text, rectangles, or circles.") + "\n")
- inkex.errormsg(_("Tip: select some objects and use Path -> Object to Path to convert them to paths.") + "\n")
+ inkex.errormsg(_("Tip: Select some objects and use Path -> Object to Path to convert them to paths.") + "\n")
- def descendants(self, node, selected=False):
+ def descendants(self, node, selected=False, troubleshoot=False): # noqa: C901
nodes = []
element = EmbroideryElement(node)
@@ -148,6 +149,10 @@ class InkstitchExtension(inkex.Effect):
if node.tag == SVG_DEFS_TAG:
return []
+ # command connectors with a fill color set, will glitch into the elements list
+ if is_command(node) or node.get(CONNECTOR_TYPE):
+ return[]
+
if self.selected:
if node.get("id") in self.selected:
selected = True
@@ -156,23 +161,26 @@ class InkstitchExtension(inkex.Effect):
selected = True
for child in node:
- nodes.extend(self.descendants(child, selected))
+ nodes.extend(self.descendants(child, selected, troubleshoot))
- if selected and node.tag in EMBROIDERABLE_TAGS:
- nodes.append(node)
+ if selected:
+ if node.tag in EMBROIDERABLE_TAGS or is_embroiderable_clone(node):
+ nodes.append(node)
+ elif troubleshoot and (node.tag in NOT_EMBROIDERABLE_TAGS or is_clone(node)):
+ nodes.append(node)
return nodes
- def get_nodes(self):
- return self.descendants(self.document.getroot())
+ def get_nodes(self, troubleshoot=False):
+ return self.descendants(self.document.getroot(), troubleshoot=troubleshoot)
- def get_elements(self):
- self.elements = nodes_to_elements(self.get_nodes())
+ def get_elements(self, troubleshoot=False):
+ self.elements = nodes_to_elements(self.get_nodes(troubleshoot))
if self.elements:
return True
- else:
+ if not troubleshoot:
self.no_elements_error()
- return False
+ return False
def elements_to_patches(self, elements):
patches = []
diff --git a/lib/extensions/params.py b/lib/extensions/params.py
index a3ba7784..600a4669 100644
--- a/lib/extensions/params.py
+++ b/lib/extensions/params.py
@@ -1,19 +1,21 @@
# -*- coding: UTF-8 -*-
+import os
+import sys
from collections import defaultdict
from copy import copy
from itertools import groupby
-import os
-import sys
-
import wx
from wx.lib.scrolledpanel import ScrolledPanel
from ..commands import is_command
-from ..elements import EmbroideryElement, Fill, AutoFill, Stroke, SatinColumn
+from ..elements import (AutoFill, Clone, EmbroideryElement, Fill, Polyline,
+ SatinColumn, Stroke)
+from ..elements.clone import is_clone
from ..gui import PresetsPanel, SimulatorPreview
from ..i18n import _
+from ..svg.tags import SVG_POLYLINE_TAG
from ..utils import get_resource_dir
from .base import InkstitchExtension
@@ -465,16 +467,18 @@ class Params(InkstitchExtension):
classes = []
if not is_command(node):
- if element.get_style("fill", "black") is not None:
- classes.append(AutoFill)
- classes.append(Fill)
-
- if element.get_style("stroke") is not None:
- classes.append(Stroke)
-
- if element.get_style("stroke-dasharray") is None:
- classes.append(SatinColumn)
-
+ if node.tag == SVG_POLYLINE_TAG:
+ classes.append(Polyline)
+ elif is_clone(node):
+ classes.append(Clone)
+ else:
+ if element.get_style("fill", 'black') and not element.get_style("fill-opacity", 1) == "0":
+ classes.append(AutoFill)
+ classes.append(Fill)
+ if element.get_style("stroke") is not None:
+ classes.append(Stroke)
+ if element.get_style("stroke-dasharray") is None:
+ classes.append(SatinColumn)
return classes
def get_nodes_by_class(self):
diff --git a/lib/extensions/remove_embroidery_settings.py b/lib/extensions/remove_embroidery_settings.py
index d39c7e94..2a4d06dd 100644
--- a/lib/extensions/remove_embroidery_settings.py
+++ b/lib/extensions/remove_embroidery_settings.py
@@ -1,6 +1,7 @@
import inkex
from ..commands import find_commands
+from ..svg.svg import find_elements
from .base import InkstitchExtension
@@ -12,6 +13,8 @@ class RemoveEmbroiderySettings(InkstitchExtension):
self.OptionParser.add_option("-d", "--del_print", dest="del_print", type="inkbool", default=False)
def effect(self):
+ self.svg = self.document.getroot()
+
if self.options.del_params:
self.remove_params()
if self.options.del_commands:
@@ -21,7 +24,7 @@ class RemoveEmbroiderySettings(InkstitchExtension):
def remove_print_settings(self):
print_settings = "svg:metadata//*"
- print_settings = self.find_elements(print_settings)
+ print_settings = find_elements(self.svg, print_settings)
for print_setting in print_settings:
if print_setting.prefix == "inkstitch":
self.remove_element(print_setting)
@@ -29,7 +32,7 @@ class RemoveEmbroiderySettings(InkstitchExtension):
def remove_params(self):
if not self.selected:
xpath = ".//svg:path"
- elements = self.find_elements(xpath)
+ elements = find_elements(self.svg, xpath)
self.remove_inkstitch_attributes(elements)
else:
for node in self.selected:
@@ -41,7 +44,7 @@ class RemoveEmbroiderySettings(InkstitchExtension):
# we are not able to grab commands by a specific id
# so let's move through every object instead and see if it has a command
xpath = ".//svg:path|.//svg:circle|.//svg:rect|.//svg:ellipse"
- elements = self.find_elements(xpath)
+ elements = find_elements(self.svg, xpath)
else:
elements = []
for node in self.selected:
@@ -64,19 +67,14 @@ class RemoveEmbroiderySettings(InkstitchExtension):
def get_selected_elements(self, element_id):
xpath = ".//svg:g[@id='%(id)s']//svg:path|.//svg:g[@id='%(id)s']//svg:use" % dict(id=element_id)
- elements = self.find_elements(xpath)
+ elements = find_elements(self.svg, xpath)
if not elements:
xpath = ".//*[@id='%s']" % element_id
- elements = self.find_elements(xpath)
- return elements
-
- def find_elements(self, xpath):
- svg = self.document.getroot()
- elements = svg.xpath(xpath, namespaces=inkex.NSS)
+ elements = find_elements(self.svg, xpath)
return elements
def remove_elements(self, xpath):
- elements = self.find_elements(xpath)
+ elements = find_elements(self.svg, xpath)
for element in elements:
self.remove_element(element)
diff --git a/lib/extensions/simulator.py b/lib/extensions/simulator.py
index 1c0627ba..66be752b 100644
--- a/lib/extensions/simulator.py
+++ b/lib/extensions/simulator.py
@@ -9,6 +9,8 @@ class Simulator(InkstitchExtension):
InkstitchExtension.__init__(self)
def effect(self):
+ if not self.get_elements():
+ return
api_server = APIServer(self)
port = api_server.start_server()
electron = open_url("/simulator?port=%d" % port)
diff --git a/lib/extensions/troubleshoot.py b/lib/extensions/troubleshoot.py
index b67a5dc1..6b63390a 100644
--- a/lib/extensions/troubleshoot.py
+++ b/lib/extensions/troubleshoot.py
@@ -1,43 +1,48 @@
-from itertools import chain
import textwrap
import inkex
from ..commands import add_layer_commands
-from ..elements.validation import ValidationWarning, ValidationError
+from ..elements.validation import (ObjectTypeWarning, ValidationError,
+ ValidationWarning)
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 ..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:
+ problem_types = {'error': set(), 'warning': set(), 'type_warning': set()}
+
+ if self.get_elements(True):
+ for element in self.elements:
+ for problem in element.validation_errors():
+ problem_types['error'].add(type(problem))
+ self.insert_pointer(problem)
+ for problem in element.validation_warnings():
+ if isinstance(problem, ObjectTypeWarning):
+ problem_types['type_warning'].add(type(problem))
+ else:
+ problem_types['warning'].add(type(problem))
+ self.insert_pointer(problem)
+
+ if any(problem_types.values()):
self.add_descriptions(problem_types)
else:
svg = self.document.getroot()
svg.remove(self.troubleshoot_layer)
- message = _("All selected shapes are valid!")
+ 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.")
-
+ message += _("If you are still having trouble with a shape not being embroidered, "
+ "check if it is in a layer with an ignore command.")
inkex.errormsg(message)
def insert_pointer(self, problem):
@@ -49,9 +54,12 @@ class Troubleshoot(InkstitchExtension):
elif isinstance(problem, ValidationError):
fill_color = "#ff0000"
layer = self.error_group
+ elif isinstance(problem, ObjectTypeWarning):
+ fill_color = "#ff9900"
+ layer = self.type_warning_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)
+ pointer_style = "stroke:#000000;stroke-width:0.2;fill:%s;" % (fill_color)
+ text_style = "fill:%s;stroke:#000000;stroke-width:0.2;font-size:8px;text-align:center;text-anchor:middle" % (fill_color)
path = inkex.etree.Element(
SVG_PATH_TAG,
@@ -119,13 +127,23 @@ class Troubleshoot(InkstitchExtension):
})
layer.append(warning_group)
+ type_warning_group = inkex.etree.SubElement(
+ layer,
+ SVG_GROUP_TAG,
+ {
+ "id": '__validation_ignored__',
+ INKSCAPE_LABEL: _("Type Warnings"),
+ })
+ layer.append(type_warning_group)
+
self.troubleshoot_layer = layer
self.error_group = error_group
self.warning_group = warning_group
+ self.type_warning_group = type_warning_group
def add_descriptions(self, problem_types):
svg = self.document.getroot()
- text_x = str(self.unittouu(svg.get('width')) + 5)
+ text_x = str(float(svg.get('viewBox', '0 0 800 0').split(' ')[2]) + 5.0)
text_container = inkex.etree.Element(
SVG_TEXT_TAG,
@@ -138,23 +156,40 @@ class Troubleshoot(InkstitchExtension):
self.troubleshoot_layer.append(text_container)
text = [
- [_("Troubleshoot"), "font-weight: bold; font-size: 6px;"],
+ [_("Troubleshoot"), "font-weight: bold; font-size: 8px;"],
["", ""]
]
- for problem in problem_types:
- text_color = "#ff0000"
- if issubclass(problem, ValidationWarning):
+ for problem_type, problems in problem_types.items():
+ if problem_type == "error":
+ text_color = "#ff0000"
+ problem_type_header = _("Errors")
+ problem_type_description = _("Problems that will prevent the shape from being embroidered.")
+ elif problem_type == "warning":
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(["", ""])
+ problem_type_header = _("Warnings")
+ problem_type_description = _("These are problems that won't prevent the shape from being embroidered. "
+ "You should consider to fix the warning, but if you don't, "
+ "Ink/Stitch will do its best to process the object.")
+ elif problem_type == "type_warning":
+ text_color = "#ff9900"
+ problem_type_header = _("Object Type Warnings")
+ problem_type_description = _("Ink/Stitch only knows how to works with paths and ignores everything else. "
+ "You might want these shapes to be ignored, but if you don't, "
+ "follow the instructions to change this behaviour.")
+ if problems:
+ text.append([problem_type_header, "font-weight: bold; fill: %s; text-decoration: underline; font-size: 7px;" % text_color])
+ text.append(["", ""])
+ text.append([problem_type_description, "fill:%s;" % text_color])
+ text.append(["", ""])
+
+ for problem in problems:
+ text.append([problem.name, "font-weight: bold; fill: %s;" % text_color])
+ text.append([problem.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, ' +
@@ -162,7 +197,9 @@ class Troubleshoot(InkstitchExtension):
'"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;"])
+ text.append([description, "font-style: italic; font-size: 4px;"])
+
+ text = self.split_text(text)
for text_line in text:
tspan = inkex.etree.Element(
@@ -174,3 +211,14 @@ class Troubleshoot(InkstitchExtension):
)
tspan.text = text_line[0]
text_container.append(tspan)
+
+ def split_text(self, text):
+ splitted_text = []
+ for text_part, style in text:
+ if text_part:
+ description_parts = textwrap.wrap(text_part, 60)
+ for description in description_parts:
+ splitted_text.append([description, style])
+ else:
+ splitted_text.append(["", ""])
+ return splitted_text
diff --git a/lib/svg/path.py b/lib/svg/path.py
index f0f6708b..817c2972 100644
--- a/lib/svg/path.py
+++ b/lib/svg/path.py
@@ -36,6 +36,9 @@ def get_node_transform(node):
# combine this node's transform with all parent groups' transforms
transform = compose_parent_transforms(node, transform)
+ if node.get('id', '').startswith('clone_'):
+ transform = simpletransform.parseTransform(node.get('transform', ''))
+
# add in the transform implied by the viewBox
viewbox_transform = get_viewbox_transform(node.getroottree().getroot())
transform = simpletransform.composeTransform(viewbox_transform, transform)
diff --git a/lib/svg/svg.py b/lib/svg/svg.py
index 464a2a18..3cf7f017 100644
--- a/lib/svg/svg.py
+++ b/lib/svg/svg.py
@@ -1,4 +1,4 @@
-from inkex import etree
+from inkex import NSS, etree
from ..utils import cache
@@ -24,3 +24,9 @@ def generate_unique_id(document_or_element, prefix="path"):
i += 1
return new_id
+
+
+def find_elements(node, xpath):
+ document = get_document(node)
+ elements = document.xpath(xpath, namespaces=NSS)
+ return elements
diff --git a/lib/svg/tags.py b/lib/svg/tags.py
index 3e444513..589f489e 100644
--- a/lib/svg/tags.py
+++ b/lib/svg/tags.py
@@ -7,12 +7,16 @@ inkex.NSS['inkstitch'] = 'http://inkstitch.org/namespace'
SVG_PATH_TAG = inkex.addNS('path', 'svg')
SVG_POLYLINE_TAG = inkex.addNS('polyline', 'svg')
+SVG_RECT_TAG = inkex.addNS('rect', 'svg')
+SVG_ELLIPSE_TAG = inkex.addNS('ellipse', 'svg')
+SVG_CIRCLE_TAG = inkex.addNS('circle', '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')
SVG_USE_TAG = inkex.addNS('use', 'svg')
+SVG_IMAGE_TAG = inkex.addNS('image', 'svg')
EMBROIDERABLE_TAGS = (SVG_PATH_TAG, SVG_POLYLINE_TAG)
@@ -30,12 +34,17 @@ SODIPODI_ROLE = inkex.addNS('role', 'sodipodi')
INKSTITCH_LETTERING = inkex.addNS('lettering', 'inkstitch')
+EMBROIDERABLE_TAGS = (SVG_PATH_TAG, SVG_POLYLINE_TAG, SVG_RECT_TAG, SVG_ELLIPSE_TAG, SVG_CIRCLE_TAG)
+NOT_EMBROIDERABLE_TAGS = (SVG_IMAGE_TAG, SVG_TEXT_TAG)
+SVG_OBJECT_TAGS = (SVG_ELLIPSE_TAG, SVG_CIRCLE_TAG, SVG_RECT_TAG)
+
INKSTITCH_ATTRIBS = {}
-# Fill
inkstitch_attribs = [
'ties',
'trim_after',
'stop_after',
+ # clone
+ 'clone',
# fill
'angle',
'auto_fill',