2021-03-12 04:17:19 +00:00
|
|
|
# Authors: see git history
|
|
|
|
#
|
|
|
|
# Copyright (c) 2010 Authors
|
|
|
|
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
|
|
|
|
|
2019-02-16 01:45:39 +00:00
|
|
|
import sys
|
2020-03-19 16:37:47 +00:00
|
|
|
from copy import deepcopy
|
2019-02-16 01:45:39 +00:00
|
|
|
|
2021-03-04 17:40:53 +00:00
|
|
|
import inkex
|
|
|
|
from inkex import bezier
|
2018-03-31 00:37:11 +00:00
|
|
|
|
2019-02-16 01:45:39 +00:00
|
|
|
from ..commands import find_commands
|
2018-05-02 01:21:07 +00:00
|
|
|
from ..i18n import _
|
2021-06-30 12:05:13 +00:00
|
|
|
from ..patterns import apply_patterns
|
2021-01-23 08:39:33 +00:00
|
|
|
from ..svg import (PIXELS_PER_MM, apply_transforms, convert_length,
|
|
|
|
get_node_transform)
|
2021-07-25 05:24:34 +00:00
|
|
|
from ..svg.tags import INKSCAPE_LABEL, INKSTITCH_ATTRIBS
|
2021-01-23 08:39:33 +00:00
|
|
|
from ..utils import Point, cache
|
2018-03-31 00:37:11 +00:00
|
|
|
|
2018-05-02 01:21:07 +00:00
|
|
|
|
2018-03-31 00:37:11 +00:00
|
|
|
class Param(object):
|
2021-03-14 08:38:36 +00:00
|
|
|
def __init__(self, name, description, unit=None, values=[], type=None, group=None, inverse=False,
|
2021-10-21 14:24:40 +00:00
|
|
|
options=[], default=None, tooltip=None, sort_index=0, select_items=None):
|
2018-03-31 00:37:11 +00:00
|
|
|
self.name = name
|
|
|
|
self.description = description
|
|
|
|
self.unit = unit
|
|
|
|
self.values = values or [""]
|
|
|
|
self.type = type
|
|
|
|
self.group = group
|
|
|
|
self.inverse = inverse
|
2021-03-14 08:38:36 +00:00
|
|
|
self.options = options
|
2018-03-31 00:37:11 +00:00
|
|
|
self.default = default
|
|
|
|
self.tooltip = tooltip
|
|
|
|
self.sort_index = sort_index
|
2021-10-21 14:24:40 +00:00
|
|
|
self.select_items = select_items
|
2018-03-31 00:37:11 +00:00
|
|
|
|
|
|
|
def __repr__(self):
|
|
|
|
return "Param(%s)" % vars(self)
|
|
|
|
|
|
|
|
|
|
|
|
# Decorate a member function or property with information about
|
|
|
|
# the embroidery parameter it corresponds to
|
|
|
|
def param(*args, **kwargs):
|
|
|
|
p = Param(*args, **kwargs)
|
|
|
|
|
|
|
|
def decorator(func):
|
|
|
|
func.param = p
|
|
|
|
return func
|
|
|
|
|
|
|
|
return decorator
|
|
|
|
|
|
|
|
|
|
|
|
class EmbroideryElement(object):
|
|
|
|
def __init__(self, node):
|
|
|
|
self.node = node
|
|
|
|
|
2021-03-14 08:38:36 +00:00
|
|
|
# update legacy embroider_ attributes to namespaced attributes
|
2020-04-25 12:45:27 +00:00
|
|
|
legacy_attribs = False
|
|
|
|
for attrib in self.node.attrib:
|
|
|
|
if attrib.startswith('embroider_'):
|
|
|
|
self.replace_legacy_param(attrib)
|
|
|
|
legacy_attribs = True
|
2021-03-14 08:38:36 +00:00
|
|
|
# convert legacy tie setting
|
2021-03-22 16:06:48 +00:00
|
|
|
legacy_tie = self.get_param('ties', None)
|
|
|
|
if legacy_tie == "True":
|
2021-03-14 08:38:36 +00:00
|
|
|
self.set_param('ties', 0)
|
2021-03-22 16:06:48 +00:00
|
|
|
elif legacy_tie == "False":
|
|
|
|
self.set_param('ties', 3)
|
|
|
|
|
2021-07-29 18:52:44 +00:00
|
|
|
# default setting for fill_underlay has changed
|
2020-04-25 12:45:27 +00:00
|
|
|
if legacy_attribs and not self.get_param('fill_underlay', ""):
|
|
|
|
self.set_param('fill_underlay', False)
|
|
|
|
|
2018-03-31 00:37:11 +00:00
|
|
|
@property
|
|
|
|
def id(self):
|
|
|
|
return self.node.get('id')
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def get_params(cls):
|
|
|
|
params = []
|
|
|
|
for attr in dir(cls):
|
|
|
|
prop = getattr(cls, attr)
|
|
|
|
if isinstance(prop, property):
|
|
|
|
# The 'param' attribute is set by the 'param' decorator defined above.
|
|
|
|
if hasattr(prop.fget, 'param'):
|
|
|
|
params.append(prop.fget.param)
|
|
|
|
return params
|
|
|
|
|
2020-04-25 12:45:27 +00:00
|
|
|
def replace_legacy_param(self, param):
|
2022-01-30 14:48:51 +00:00
|
|
|
# remove "embroider_" prefix
|
|
|
|
new_param = param[10:]
|
|
|
|
if new_param in INKSTITCH_ATTRIBS:
|
|
|
|
value = self.node.get(param, "").strip()
|
|
|
|
self.set_param(param[10:], value)
|
2020-04-25 12:45:27 +00:00
|
|
|
del self.node.attrib[param]
|
|
|
|
|
2018-03-31 00:37:11 +00:00
|
|
|
@cache
|
|
|
|
def get_param(self, param, default):
|
2020-04-25 12:45:27 +00:00
|
|
|
value = self.node.get(INKSTITCH_ATTRIBS[param], "").strip()
|
2018-03-31 00:37:11 +00:00
|
|
|
return value or default
|
|
|
|
|
|
|
|
@cache
|
|
|
|
def get_boolean_param(self, param, default=None):
|
|
|
|
value = self.get_param(param, default)
|
|
|
|
|
|
|
|
if isinstance(value, bool):
|
|
|
|
return value
|
|
|
|
else:
|
|
|
|
return value and (value.lower() in ('yes', 'y', 'true', 't', '1'))
|
|
|
|
|
|
|
|
@cache
|
|
|
|
def get_float_param(self, param, default=None):
|
|
|
|
try:
|
|
|
|
value = float(self.get_param(param, default))
|
|
|
|
except (TypeError, ValueError):
|
|
|
|
value = default
|
|
|
|
|
|
|
|
if value is None:
|
|
|
|
return value
|
|
|
|
|
|
|
|
if param.endswith('_mm'):
|
|
|
|
value = value * PIXELS_PER_MM
|
|
|
|
|
|
|
|
return value
|
|
|
|
|
|
|
|
@cache
|
|
|
|
def get_int_param(self, param, default=None):
|
|
|
|
try:
|
|
|
|
value = int(self.get_param(param, default))
|
|
|
|
except (TypeError, ValueError):
|
|
|
|
return default
|
|
|
|
|
|
|
|
if param.endswith('_mm'):
|
|
|
|
value = int(value * PIXELS_PER_MM)
|
|
|
|
|
|
|
|
return value
|
|
|
|
|
|
|
|
def set_param(self, name, value):
|
2020-04-25 12:45:27 +00:00
|
|
|
param = INKSTITCH_ATTRIBS[name]
|
|
|
|
self.node.set(param, str(value))
|
2018-03-31 00:37:11 +00:00
|
|
|
|
|
|
|
@cache
|
2021-07-25 05:24:34 +00:00
|
|
|
def _get_specified_style(self):
|
|
|
|
# We want to cache this, because it's quite expensive to generate.
|
|
|
|
return self.node.specified_style()
|
2020-04-25 12:22:17 +00:00
|
|
|
|
2018-06-10 19:13:51 +00:00
|
|
|
def get_style(self, style_name, default=None):
|
2021-07-25 05:24:34 +00:00
|
|
|
style = self._get_specified_style().get(style_name, default)
|
2020-04-25 12:22:17 +00:00
|
|
|
if style == 'none':
|
|
|
|
style = None
|
|
|
|
return style
|
2018-03-31 00:37:11 +00:00
|
|
|
|
|
|
|
def has_style(self, style_name):
|
2020-05-02 13:00:42 +00:00
|
|
|
return self._get_style_raw(style_name) is not None
|
2018-03-31 00:37:11 +00:00
|
|
|
|
2018-05-02 01:21:07 +00:00
|
|
|
@property
|
|
|
|
@cache
|
|
|
|
def stroke_scale(self):
|
2021-01-23 08:39:33 +00:00
|
|
|
# How wide is the stroke, after the transforms are applied?
|
|
|
|
#
|
|
|
|
# If the transform is just simple scaling that preserves the aspect ratio,
|
|
|
|
# then this is completely accurate. If there's uneven scaling or skewing,
|
|
|
|
# then the stroke is bent out of shape. We'll make an approximation based on
|
|
|
|
# the average scaling in the X and Y axes.
|
|
|
|
#
|
|
|
|
# Of course, transforms may also involve rotation, skewing, and translation.
|
|
|
|
# All except translation can affect how wide the stroke appears on the screen.
|
|
|
|
|
2022-05-07 20:20:15 +00:00
|
|
|
node_transform = inkex.transforms.Transform(get_node_transform(self.node))
|
2021-01-23 08:39:33 +00:00
|
|
|
|
|
|
|
# First, figure out the translation component of the transform. Using a zero
|
|
|
|
# vector completely cancels out the rotation, scale, and skew components.
|
|
|
|
zero = [0, 0]
|
2021-03-04 17:40:53 +00:00
|
|
|
zero = inkex.Transform.apply_to_point(node_transform, zero)
|
2021-01-23 08:39:33 +00:00
|
|
|
translate = Point(*zero)
|
|
|
|
|
|
|
|
# Next, see how the transform affects unit vectors in the X and Y axes. We
|
|
|
|
# need to subtract off the translation or it will affect the magnitude of
|
|
|
|
# the resulting vector, which we don't want.
|
|
|
|
unit_x = [1, 0]
|
2021-03-04 17:40:53 +00:00
|
|
|
unit_x = inkex.Transform.apply_to_point(node_transform, unit_x)
|
2021-01-23 08:39:33 +00:00
|
|
|
sx = (Point(*unit_x) - translate).length()
|
|
|
|
|
|
|
|
unit_y = [0, 1]
|
2021-03-04 17:40:53 +00:00
|
|
|
unit_y = inkex.Transform.apply_to_point(node_transform, unit_y)
|
2021-01-23 08:39:33 +00:00
|
|
|
sy = (Point(*unit_y) - translate).length()
|
|
|
|
|
|
|
|
# Take the average as a best guess.
|
|
|
|
node_scale = (sx + sy) / 2.0
|
|
|
|
|
|
|
|
return node_scale
|
2018-05-02 01:21:07 +00:00
|
|
|
|
2018-03-31 00:37:11 +00:00
|
|
|
@property
|
|
|
|
@cache
|
|
|
|
def stroke_width(self):
|
2021-03-04 17:40:53 +00:00
|
|
|
width = self.get_style("stroke-width", "1.0")
|
2018-03-31 00:37:11 +00:00
|
|
|
width = convert_length(width)
|
2018-05-02 01:21:07 +00:00
|
|
|
return width * self.stroke_scale
|
2018-03-31 00:37:11 +00:00
|
|
|
|
2020-03-19 16:37:47 +00:00
|
|
|
@property
|
|
|
|
@param('ties',
|
2021-03-14 08:38:36 +00:00
|
|
|
_('Allow lock stitches'),
|
2022-05-07 20:20:15 +00:00
|
|
|
tooltip=_('Tie thread at the beginning and/or end of this object. Manual stitch will not add lock stitches.'),
|
2021-03-14 08:38:36 +00:00
|
|
|
type='dropdown',
|
|
|
|
# Ties: 0 = Both | 1 = Before | 2 = After | 3 = Neither
|
|
|
|
# L10N options to allow lock stitch before and after objects
|
|
|
|
options=[_("Both"), _("Before"), _("After"), _("Neither")],
|
|
|
|
default=0,
|
2022-05-06 02:53:31 +00:00
|
|
|
sort_index=10)
|
2020-03-19 16:37:47 +00:00
|
|
|
@cache
|
|
|
|
def ties(self):
|
2021-03-14 08:38:36 +00:00
|
|
|
return self.get_int_param("ties", 0)
|
2020-03-19 16:37:47 +00:00
|
|
|
|
2021-12-09 14:05:21 +00:00
|
|
|
@property
|
|
|
|
@param('force_lock_stitches',
|
|
|
|
_('Force lock stitches'),
|
|
|
|
tooltip=_('Sew lock stitches after sewing this element, '
|
|
|
|
'even if the distance to the next object is shorter than defined by the collapse length value in the Ink/Stitch preferences.'),
|
|
|
|
type='boolean',
|
|
|
|
default=False,
|
2022-05-06 02:53:31 +00:00
|
|
|
sort_index=10)
|
2021-12-09 14:05:21 +00:00
|
|
|
@cache
|
|
|
|
def force_lock_stitches(self):
|
|
|
|
return self.get_boolean_param('force_lock_stitches', False)
|
|
|
|
|
2018-03-31 00:37:11 +00:00
|
|
|
@property
|
|
|
|
def path(self):
|
|
|
|
# A CSP is a "cubic superpath".
|
|
|
|
#
|
|
|
|
# A "path" is a sequence of strung-together bezier curves.
|
|
|
|
#
|
|
|
|
# A "superpath" is a collection of paths that are all in one object.
|
|
|
|
#
|
|
|
|
# The "cubic" bit in "cubic superpath" is because the bezier curves
|
|
|
|
# inkscape uses involve cubic polynomials.
|
|
|
|
#
|
|
|
|
# Each path is a collection of tuples, each of the form:
|
|
|
|
#
|
|
|
|
# (control_before, point, control_after)
|
|
|
|
#
|
|
|
|
# A bezier curve segment is defined by an endpoint, a control point,
|
|
|
|
# a second control point, and a final endpoint. A path is a bunch of
|
|
|
|
# bezier curves strung together. One could represent a path as a set
|
|
|
|
# of four-tuples, but there would be redundancy because the ending
|
|
|
|
# point of one bezier is the starting point of the next. Instead, a
|
|
|
|
# path is a set of 3-tuples as shown above, and one must construct
|
|
|
|
# each bezier curve by taking the appropriate endpoints and control
|
|
|
|
# points. Bleh. It should be noted that a straight segment is
|
|
|
|
# represented by having the control point on each end equal to that
|
|
|
|
# end's point.
|
|
|
|
#
|
|
|
|
# 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?
|
|
|
|
|
2021-03-04 17:40:53 +00:00
|
|
|
if getattr(self.node, "get_path", None):
|
|
|
|
d = self.node.get_path()
|
2020-05-16 21:01:00 +00:00
|
|
|
else:
|
|
|
|
d = self.node.get("d", "")
|
|
|
|
|
2018-08-25 01:20:59 +00:00
|
|
|
if not d:
|
2022-05-07 20:20:15 +00:00
|
|
|
self.fatal(_("Object %(id)s has an empty 'd' attribute. Please delete this object from your document.") % dict(id=self.node.get("id")))
|
2018-08-25 01:20:59 +00:00
|
|
|
|
2021-03-04 17:40:53 +00:00
|
|
|
return inkex.paths.Path(d).to_superpath()
|
2018-03-31 00:37:11 +00:00
|
|
|
|
2018-06-21 19:41:06 +00:00
|
|
|
@cache
|
|
|
|
def parse_path(self):
|
|
|
|
return apply_transforms(self.path, self.node)
|
2018-03-31 00:37:11 +00:00
|
|
|
|
2022-01-30 14:48:51 +00:00
|
|
|
@property
|
|
|
|
@cache
|
|
|
|
def paths(self):
|
|
|
|
return self.flatten(self.parse_path())
|
|
|
|
|
2018-07-05 01:17:20 +00:00
|
|
|
@property
|
|
|
|
def shape(self):
|
2022-05-07 20:20:15 +00:00
|
|
|
raise NotImplementedError("INTERNAL ERROR: %s must implement shape()", self.__class__)
|
2018-07-05 01:17:20 +00:00
|
|
|
|
2018-06-21 19:41:06 +00:00
|
|
|
@property
|
|
|
|
@cache
|
|
|
|
def commands(self):
|
|
|
|
return find_commands(self.node)
|
2018-03-31 00:37:11 +00:00
|
|
|
|
2018-06-23 02:19:57 +00:00
|
|
|
@cache
|
|
|
|
def get_commands(self, command):
|
|
|
|
return [c for c in self.commands if c.command == command]
|
|
|
|
|
2018-06-30 18:16:56 +00:00
|
|
|
@cache
|
|
|
|
def has_command(self, command):
|
|
|
|
return len(self.get_commands(command)) > 0
|
|
|
|
|
2018-06-23 02:19:57 +00:00
|
|
|
@cache
|
|
|
|
def get_command(self, command):
|
|
|
|
commands = self.get_commands(command)
|
|
|
|
|
2021-03-22 16:06:48 +00:00
|
|
|
if commands:
|
2018-06-23 02:19:57 +00:00
|
|
|
return commands[0]
|
|
|
|
else:
|
|
|
|
return None
|
|
|
|
|
2018-04-03 02:11:57 +00:00
|
|
|
def strip_control_points(self, subpath):
|
|
|
|
return [point for control_before, point, control_after in subpath]
|
|
|
|
|
2018-03-31 00:37:11 +00:00
|
|
|
def flatten(self, path):
|
|
|
|
"""approximate a path containing beziers with a series of points"""
|
|
|
|
|
|
|
|
path = deepcopy(path)
|
2021-03-04 17:40:53 +00:00
|
|
|
bezier.cspsubdiv(path, 0.1)
|
2018-03-31 00:37:11 +00:00
|
|
|
|
2018-04-03 02:11:57 +00:00
|
|
|
return [self.strip_control_points(subpath) for subpath in path]
|
2018-03-31 00:37:11 +00:00
|
|
|
|
2018-09-29 20:00:36 +00:00
|
|
|
def flatten_subpath(self, subpath):
|
|
|
|
path = [deepcopy(subpath)]
|
2021-03-04 17:40:53 +00:00
|
|
|
bezier.cspsubdiv(path, 0.1)
|
2018-09-29 20:00:36 +00:00
|
|
|
|
|
|
|
return self.strip_control_points(path[0])
|
|
|
|
|
2018-03-31 00:37:11 +00:00
|
|
|
@property
|
|
|
|
def trim_after(self):
|
|
|
|
return self.get_boolean_param('trim_after', False)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def stop_after(self):
|
|
|
|
return self.get_boolean_param('stop_after', False)
|
|
|
|
|
2021-08-07 16:37:17 +00:00
|
|
|
def to_stitch_groups(self, last_patch):
|
2022-05-07 20:20:15 +00:00
|
|
|
raise NotImplementedError("%s must implement to_stitch_groups()" % self.__class__.__name__)
|
2018-03-31 00:37:11 +00:00
|
|
|
|
|
|
|
def embroider(self, last_patch):
|
2019-08-06 02:42:48 +00:00
|
|
|
self.validate()
|
|
|
|
|
2021-08-07 16:37:17 +00:00
|
|
|
patches = self.to_stitch_groups(last_patch)
|
2021-06-30 12:05:13 +00:00
|
|
|
apply_patterns(patches, self.node)
|
2018-03-31 00:37:11 +00:00
|
|
|
|
2021-03-14 08:38:36 +00:00
|
|
|
for patch in patches:
|
|
|
|
patch.tie_modus = self.ties
|
2021-12-09 14:05:21 +00:00
|
|
|
patch.force_lock_stitches = self.force_lock_stitches
|
2020-03-19 16:37:47 +00:00
|
|
|
|
2018-03-31 00:37:11 +00:00
|
|
|
if patches:
|
2022-05-07 20:20:15 +00:00
|
|
|
patches[-1].trim_after = self.has_command("trim") or self.trim_after
|
|
|
|
patches[-1].stop_after = self.has_command("stop") or self.stop_after
|
2018-03-31 00:37:11 +00:00
|
|
|
|
|
|
|
return patches
|
|
|
|
|
|
|
|
def fatal(self, message):
|
2019-02-16 01:45:39 +00:00
|
|
|
label = self.node.get(INKSCAPE_LABEL)
|
|
|
|
id = self.node.get("id")
|
|
|
|
if label:
|
|
|
|
name = "%s (%s)" % (label, id)
|
|
|
|
else:
|
|
|
|
name = id
|
|
|
|
|
|
|
|
# L10N used when showing an error message to the user such as
|
|
|
|
# "Some Path (path1234): error: satin column: One or more of the rungs doesn't intersect both rails."
|
2019-06-24 16:54:43 +00:00
|
|
|
error_msg = "%s: %s %s" % (name, _("error:"), message)
|
2021-03-04 17:40:53 +00:00
|
|
|
inkex.errormsg(error_msg)
|
2018-03-31 00:37:11 +00:00
|
|
|
sys.exit(1)
|
2019-08-06 02:42:48 +00:00
|
|
|
|
|
|
|
def validation_errors(self):
|
|
|
|
"""Return a list of errors with this Element.
|
|
|
|
|
|
|
|
Validation errors will prevent the Element from being stitched.
|
|
|
|
|
|
|
|
Return value: an iterable or generator of instances of subclasses of ValidationError
|
|
|
|
"""
|
|
|
|
return []
|
|
|
|
|
|
|
|
def validation_warnings(self):
|
|
|
|
"""Return a list of warnings about this Element.
|
|
|
|
|
|
|
|
Validation warnings don't prevent the Element from being stitched but
|
|
|
|
the user should probably fix them anyway.
|
|
|
|
|
|
|
|
Return value: an iterable or generator of instances of subclasses of ValidationWarning
|
|
|
|
"""
|
|
|
|
return []
|
|
|
|
|
|
|
|
def is_valid(self):
|
|
|
|
# We have to iterate since it could be a generator.
|
|
|
|
for error in self.validation_errors():
|
|
|
|
return False
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
def validate(self):
|
|
|
|
"""Print an error message and exit if this Element is invalid."""
|
|
|
|
|
|
|
|
for error in self.validation_errors():
|
|
|
|
# note that self.fatal() exits, so this only shows the first error
|
|
|
|
self.fatal(error.description)
|