diff --git a/lib/commands.py b/lib/commands.py index b92d79cff..9d0b243c2 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 5413ba04d..75509e296 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 000000000..0e7a5d63c --- /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 62f600d6a..f5f774f0d 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 0f72d0009..59b7414bf 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 000000000..ec8d1765c --- /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 a9870172b..2d008d357 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 000000000..e597f7c1b --- /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 000000000..2d066bb03 --- /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 5c71de2e7..4719a5ff4 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 41098922d..f77e2fc4c 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 440a54132..310dd8731 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 a3ba77848..600a4669d 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 d39c7e944..2a4d06dd7 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 1c0627ba0..66be752bf 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 b67a5dc16..6b63390a9 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) + problem_types = {'error': set(), 'warning': set(), 'type_warning': set()} - if problem_types: + 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" + 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(["", ""]) - 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(["", ""]) + 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 f0f6708bf..817c29722 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 464a2a18a..3cf7f0173 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 3e4445131..589f489ed 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',