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/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
10 files changed, 376 insertions, 19 deletions
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