inkstitch/lib/elements/element.py

367 wiersze
12 KiB
Python
Czysty Zwykły widok Historia

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
import cubicsuperpath
2020-04-25 12:22:17 +00:00
import tinycss2
2020-03-19 16:37:47 +00:00
from cspsubdiv import cspsubdiv
2019-02-16 01:45:39 +00:00
from ..commands import find_commands
from ..i18n import _
from ..svg import PIXELS_PER_MM, apply_transforms, convert_length, get_doc_size
2020-04-25 12:45:27 +00:00
from ..svg.tags import INKSCAPE_LABEL, INKSTITCH_ATTRIBS
2019-02-16 01:45:39 +00:00
from ..utils import cache
class Patch:
"""A raw collection of stitches with attached instructions."""
2018-04-07 02:09:26 +00:00
def __init__(self, color=None, stitches=None, trim_after=False, stop_after=False, stitch_as_is=False):
self.color = color
self.stitches = stitches or []
self.trim_after = trim_after
self.stop_after = stop_after
2018-04-07 02:09:26 +00:00
self.stitch_as_is = stitch_as_is
def __add__(self, other):
if isinstance(other, Patch):
return Patch(self.color, self.stitches + other.stitches)
else:
raise TypeError("Patch can only be added to another Patch")
2018-06-05 00:19:37 +00:00
def __len__(self):
# This method allows `len(patch)` and `if patch:
return len(self.stitches)
def add_stitch(self, stitch):
self.stitches.append(stitch)
def reverse(self):
return Patch(self.color, self.stitches[::-1])
class Param(object):
def __init__(self, name, description, unit=None, values=[], type=None, group=None, inverse=False, default=None, tooltip=None, sort_index=0):
self.name = name
self.description = description
self.unit = unit
self.values = values or [""]
self.type = type
self.group = group
self.inverse = inverse
self.default = default
self.tooltip = tooltip
self.sort_index = sort_index
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
2020-04-25 12:45:27 +00:00
legacy_attribs = False
for attrib in self.node.attrib:
if attrib.startswith('embroider_'):
# update embroider_ attributes to namespaced attributes
self.replace_legacy_param(attrib)
legacy_attribs = True
if legacy_attribs and not self.get_param('fill_underlay', ""):
# defaut setting for fill_underlay has changed
self.set_param('fill_underlay', False)
@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):
value = self.node.get(param, "").strip()
self.set_param(param[10:], value)
del self.node.attrib[param]
@cache
def get_param(self, param, default):
2020-04-25 12:45:27 +00:00
value = self.node.get(INKSTITCH_ATTRIBS[param], "").strip()
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))
@cache
2020-05-02 13:00:42 +00:00
def parse_style(self, node=None):
if node is None:
node = self.node
declarations = tinycss2.parse_declaration_list(node.get("style", ""))
2020-04-25 12:22:17 +00:00
style = {declaration.lower_name: declaration.value[0].serialize() for declaration in declarations}
2020-05-02 13:00:42 +00:00
return style
2020-04-25 12:22:17 +00:00
2020-05-02 13:00:42 +00:00
@cache
def _get_style_raw(self, style_name):
style = self.parse_style()
style = style.get(style_name) or self.node.get(style_name)
parent = self.node.getparent()
# style not found, get inherited style elements
while not style and parent is not None:
style = self.parse_style(parent)
style = style.get(style_name) or parent.get(style_name)
parent = parent.getparent()
2020-04-25 12:22:17 +00:00
return style
2018-06-10 19:13:51 +00:00
def get_style(self, style_name, default=None):
2020-05-02 13:00:42 +00:00
style = self._get_style_raw(style_name) or default
2020-04-25 12:22:17 +00:00
if style == 'none':
style = None
return style
def has_style(self, style_name):
2020-05-02 13:00:42 +00:00
return self._get_style_raw(style_name) is not None
@property
@cache
def stroke_scale(self):
svg = self.node.getroottree().getroot()
doc_width, doc_height = get_doc_size(svg)
viewbox = svg.get('viewBox', '0 0 %s %s' % (doc_width, doc_height))
viewbox = viewbox.strip().replace(',', ' ').split()
2018-08-22 00:32:50 +00:00
return doc_width / float(viewbox[2])
@property
@cache
def stroke_width(self):
2020-04-25 12:22:17 +00:00
width = self.get_style("stroke-width", None)
if width is None:
return 1.0
width = convert_length(width)
return width * self.stroke_scale
2020-03-19 16:37:47 +00:00
@property
@param('ties',
_('Ties'),
tooltip=_('Add ties. Manual stitch will not add ties.'),
type='boolean',
default=True,
sort_index=4)
@cache
def ties(self):
return self.get_boolean_param("ties", True)
@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?
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")))
return cubicsuperpath.parsePath(d)
2018-06-21 19:41:06 +00:00
@cache
def parse_path(self):
return apply_transforms(self.path, self.node)
@property
def shape(self):
raise NotImplementedError("INTERNAL ERROR: %s must implement shape()", self.__class__)
2018-06-21 19:41:06 +00:00
@property
@cache
def commands(self):
return find_commands(self.node)
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)
if len(commands) == 1:
return commands[0]
elif len(commands) > 1:
raise ValueError(_("%(id)s has more than one command of type '%(command)s' linked to it") %
2018-08-22 00:32:50 +00:00
dict(id=self.node.get(id), command=command))
2018-06-23 02:19:57 +00:00
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]
def flatten(self, path):
"""approximate a path containing beziers with a series of points"""
path = deepcopy(path)
cspsubdiv(path, 0.1)
2018-04-03 02:11:57 +00:00
return [self.strip_control_points(subpath) for subpath in path]
def flatten_subpath(self, subpath):
path = [deepcopy(subpath)]
cspsubdiv(path, 0.1)
return self.strip_control_points(path[0])
@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)
def to_patches(self, last_patch):
raise NotImplementedError("%s must implement to_patches()" % self.__class__.__name__)
def embroider(self, last_patch):
self.validate()
patches = self.to_patches(last_patch)
2020-03-19 16:37:47 +00:00
if not self.ties:
for patch in patches:
patch.stitch_as_is = True
if patches:
2018-06-30 18:18:45 +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
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)
print >> sys.stderr, "%s" % (error_msg.encode("UTF-8"))
sys.exit(1)
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)