diff options
Diffstat (limited to 'lib/elements')
| -rw-r--r-- | lib/elements/__init__.py | 3 | ||||
| -rw-r--r-- | lib/elements/clone.py | 169 | ||||
| -rw-r--r-- | lib/elements/element.py | 22 | ||||
| -rw-r--r-- | lib/elements/fill.py | 12 | ||||
| -rw-r--r-- | lib/elements/image.py | 34 | ||||
| -rw-r--r-- | lib/elements/polyline.py | 13 | ||||
| -rw-r--r-- | lib/elements/svg_objects.py | 71 | ||||
| -rw-r--r-- | lib/elements/text.py | 32 | ||||
| -rw-r--r-- | lib/elements/utils.py | 29 | ||||
| -rw-r--r-- | lib/elements/validation.py | 10 |
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 |
