kopia lustrzana https://github.com/inkstitch/inkstitch
support svg objects (#643)
rodzic
4e95033241
commit
a308db7ae1
|
@ -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%",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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")))
|
||||
|
||||
|
|
|
@ -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).")
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -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 []
|
|
@ -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):
|
||||
|
|
|
@ -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
|
|
@ -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 []
|
|
@ -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 []
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 = []
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
|
|
Ładowanie…
Reference in New Issue