kopia lustrzana https://github.com/inkstitch/inkstitch
514 wiersze
18 KiB
Python
514 wiersze
18 KiB
Python
# Authors: see git history
|
|
#
|
|
# Copyright (c) 2010 Authors
|
|
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
|
|
|
|
import sys
|
|
|
|
import shapely.geometry
|
|
|
|
from inkex import Transform
|
|
|
|
from ..i18n import _
|
|
from ..marker import get_marker_elements
|
|
from ..stitch_plan import StitchGroup
|
|
from ..stitches import bean_stitch, running_stitch
|
|
from ..stitches.ripple_stitch import ripple_stitch
|
|
from ..svg import get_node_transform, parse_length_with_units
|
|
from ..utils import Point, cache
|
|
from .element import EmbroideryElement, param
|
|
from .validation import ValidationWarning
|
|
|
|
warned_about_legacy_running_stitch = False
|
|
|
|
|
|
class IgnoreSkipValues(ValidationWarning):
|
|
name = _("Ignore skip")
|
|
description = _("Skip values are ignored, because there was no line left to embroider.")
|
|
steps_to_solve = [
|
|
_('* Open the params dialog with this object selected'),
|
|
_('* Reduce Skip values or increase number of lines'),
|
|
]
|
|
|
|
|
|
class MultipleGuideLineWarning(ValidationWarning):
|
|
name = _("Multiple Guide Lines")
|
|
description = _("This object has multiple guide lines, but only the first one will be used.")
|
|
steps_to_solve = [
|
|
_("* Remove all guide lines, except for one.")
|
|
]
|
|
|
|
|
|
class SmallZigZagWarning(ValidationWarning):
|
|
name = _("Small ZigZag")
|
|
description = _("This zig zag stitch has a stroke width smaller than 0.5 units.")
|
|
steps_to_solve = [
|
|
_("Set your stroke to be dashed to indicate running stitch. Any kind of dash will work.")
|
|
]
|
|
|
|
|
|
class Stroke(EmbroideryElement):
|
|
element_name = _("Stroke")
|
|
|
|
@property
|
|
@param('satin_column', _('Running stitch along paths'), type='toggle', inverse=True)
|
|
def satin_column(self):
|
|
return self.get_boolean_param("satin_column")
|
|
|
|
@property
|
|
def color(self):
|
|
return self.get_style("stroke")
|
|
|
|
@property
|
|
def dashed(self):
|
|
return self.get_style("stroke-dasharray") is not None
|
|
|
|
@property
|
|
@param('stroke_method',
|
|
_('Method'),
|
|
type='dropdown',
|
|
default=0,
|
|
# 0: run/simple satin, 1: manual, 2: ripple
|
|
options=[_("Running Stitch"), _("Ripple")],
|
|
sort_index=0)
|
|
def stroke_method(self):
|
|
return self.get_int_param('stroke_method', 0)
|
|
|
|
@property
|
|
@param('manual_stitch',
|
|
_('Manual stitch placement'),
|
|
tooltip=_("Stitch every node in the path. All other options are ignored."),
|
|
type='boolean',
|
|
default=False,
|
|
select_items=[('stroke_method', 0)],
|
|
sort_index=1)
|
|
def manual_stitch_mode(self):
|
|
return self.get_boolean_param('manual_stitch')
|
|
|
|
@property
|
|
@param('repeats',
|
|
_('Repeats'),
|
|
tooltip=_('Defines how many times to run down and back along the path.'),
|
|
type='int',
|
|
default="1",
|
|
sort_index=2)
|
|
def repeats(self):
|
|
return max(1, self.get_int_param("repeats", 1))
|
|
|
|
@property
|
|
@param(
|
|
'bean_stitch_repeats',
|
|
_('Bean stitch number of repeats'),
|
|
tooltip=_('Backtrack each stitch this many times. '
|
|
'A value of 1 would triple each stitch (forward, back, forward). '
|
|
'A value of 2 would quintuple each stitch, etc.'),
|
|
type='int',
|
|
default=0,
|
|
sort_index=3)
|
|
def bean_stitch_repeats(self):
|
|
return self.get_int_param("bean_stitch_repeats", 0)
|
|
|
|
@property
|
|
@param('running_stitch_length_mm',
|
|
_('Running stitch length'),
|
|
tooltip=_('Length of stitches in running stitch mode.'),
|
|
unit='mm',
|
|
type='float',
|
|
default=1.5,
|
|
sort_index=4)
|
|
def running_stitch_length(self):
|
|
return max(self.get_float_param("running_stitch_length_mm", 1.5), 0.01)
|
|
|
|
@property
|
|
@param('running_stitch_tolerance_mm',
|
|
_('Running stitch tolerance'),
|
|
tooltip=_('All stitches must be within this distance from the path. ' +
|
|
'A lower tolerance means stitches will be closer together. ' +
|
|
'A higher tolerance means sharp corners may be rounded.'),
|
|
unit='mm',
|
|
type='float',
|
|
default=0.2,
|
|
sort_index=4)
|
|
def running_stitch_tolerance(self):
|
|
return max(self.get_float_param("running_stitch_tolerance_mm", 0.2), 0.01)
|
|
|
|
@property
|
|
@param('zigzag_spacing_mm',
|
|
_('Zig-zag spacing (peak-to-peak)'),
|
|
tooltip=_('Length of stitches in zig-zag mode.'),
|
|
unit='mm',
|
|
type='float',
|
|
default=0.4,
|
|
select_items=[('stroke_method', 0)],
|
|
sort_index=5)
|
|
@cache
|
|
def zigzag_spacing(self):
|
|
return max(self.get_float_param("zigzag_spacing_mm", 0.4), 0.01)
|
|
|
|
@property
|
|
@param('line_count',
|
|
_('Number of lines'),
|
|
tooltip=_('Number of lines from start to finish'),
|
|
type='int',
|
|
default=10,
|
|
select_items=[('stroke_method', 1)],
|
|
sort_index=5)
|
|
@cache
|
|
def line_count(self):
|
|
return max(self.get_int_param("line_count", 10), 1)
|
|
|
|
def get_line_count(self):
|
|
if self.is_closed or self.join_style == 1:
|
|
return self.line_count + 1
|
|
return self.line_count
|
|
|
|
@property
|
|
@param('skip_start',
|
|
_('Skip first lines'),
|
|
tooltip=_('Skip this number of lines at the beginning.'),
|
|
type='int',
|
|
default=0,
|
|
select_items=[('stroke_method', 1)],
|
|
sort_index=6)
|
|
@cache
|
|
def skip_start(self):
|
|
return abs(self.get_int_param("skip_start", 0))
|
|
|
|
@property
|
|
@param('skip_end',
|
|
_('Skip last lines'),
|
|
tooltip=_('Skip this number of lines at the end'),
|
|
type='int',
|
|
default=0,
|
|
select_items=[('stroke_method', 1)],
|
|
sort_index=7)
|
|
@cache
|
|
def skip_end(self):
|
|
return abs(self.get_int_param("skip_end", 0))
|
|
|
|
def _adjust_skip(self, skip):
|
|
if self.skip_start + self.skip_end >= self.line_count:
|
|
return 0
|
|
else:
|
|
return skip
|
|
|
|
def get_skip_start(self):
|
|
return self._adjust_skip(self.skip_start)
|
|
|
|
def get_skip_end(self):
|
|
return self._adjust_skip(self.skip_end)
|
|
|
|
@property
|
|
@param('exponent',
|
|
_('Line distance exponent'),
|
|
tooltip=_('Increase density towards one side.'),
|
|
type='float',
|
|
default=1,
|
|
select_items=[('stroke_method', 1)],
|
|
sort_index=8)
|
|
@cache
|
|
def exponent(self):
|
|
return max(self.get_float_param("exponent", 1), 0.1)
|
|
|
|
@property
|
|
@param('flip_exponent',
|
|
_('Flip exponent'),
|
|
tooltip=_('Reverse exponent effect.'),
|
|
type='boolean',
|
|
default=False,
|
|
select_items=[('stroke_method', 1)],
|
|
sort_index=9)
|
|
@cache
|
|
def flip_exponent(self):
|
|
return self.get_boolean_param("flip_exponent", False)
|
|
|
|
@property
|
|
@param('reverse',
|
|
_('Reverse'),
|
|
tooltip=_('Flip start and end point'),
|
|
type='boolean',
|
|
default=False,
|
|
select_items=[('stroke_method', 1)],
|
|
sort_index=10)
|
|
@cache
|
|
def reverse(self):
|
|
return self.get_boolean_param("reverse", False)
|
|
|
|
@property
|
|
@param('grid_size',
|
|
_('Grid size'),
|
|
tooltip=_('Render as grid. Use with care and watch your stitch density.'),
|
|
type='float',
|
|
default=0,
|
|
unit='mm',
|
|
select_items=[('stroke_method', 1)],
|
|
sort_index=11)
|
|
@cache
|
|
def grid_size(self):
|
|
return abs(self.get_float_param("grid_size", 0))
|
|
|
|
@property
|
|
@param('scale_axis',
|
|
_('Scale axis'),
|
|
tooltip=_('Scale axis for satin guided ripple stitches.'),
|
|
type='dropdown',
|
|
default=0,
|
|
# 0: xy, 1: x, 2: y, 3: none
|
|
options=["X Y", "X", "Y", _("None")],
|
|
select_items=[('stroke_method', 1)],
|
|
sort_index=12)
|
|
def scale_axis(self):
|
|
return self.get_int_param('scale_axis', 0)
|
|
|
|
@property
|
|
@param('scale_start',
|
|
_('Starting scale'),
|
|
tooltip=_('How big the first copy of the line should be, in percent.') + " " + _('Used only for ripple stitch with a guide line.'),
|
|
type='float',
|
|
default=100,
|
|
select_items=[('stroke_method', 1)],
|
|
sort_index=13)
|
|
def scale_start(self):
|
|
return self.get_float_param('scale_start', 100.0)
|
|
|
|
@property
|
|
@param('scale_end',
|
|
_('Ending scale'),
|
|
tooltip=_('How big the last copy of the line should be, in percent.') + " " + _('Used only for ripple stitch with a guide line.'),
|
|
type='float',
|
|
default=0.0,
|
|
select_items=[('stroke_method', 1)],
|
|
sort_index=14)
|
|
def scale_end(self):
|
|
return self.get_float_param('scale_end', 0.0)
|
|
|
|
@property
|
|
@param('rotate_ripples',
|
|
_('Rotate'),
|
|
tooltip=_('Rotate satin guided ripple stitches'),
|
|
type='boolean',
|
|
default=True,
|
|
select_items=[('stroke_method', 1)],
|
|
sort_index=15)
|
|
@cache
|
|
def rotate_ripples(self):
|
|
return self.get_boolean_param("rotate_ripples", True)
|
|
|
|
@property
|
|
@param('join_style',
|
|
_('Join style'),
|
|
tooltip=_('Join style for non circular ripples.'),
|
|
type='dropdown',
|
|
default=0,
|
|
options=(_("flat"), _("point")),
|
|
select_items=[('stroke_method', 1)],
|
|
sort_index=16)
|
|
@cache
|
|
def join_style(self):
|
|
return self.get_int_param('join_style', 0)
|
|
|
|
@property
|
|
@cache
|
|
def is_closed(self):
|
|
# returns true if the outline of a single line stroke is a closed shape
|
|
# (with a small tolerance)
|
|
lines = self.as_multi_line_string().geoms
|
|
if len(lines) == 1:
|
|
coords = lines[0].coords
|
|
return Point(*coords[0]).distance(Point(*coords[-1])) < 0.05
|
|
return False
|
|
|
|
@property
|
|
def paths(self):
|
|
path = self.parse_path()
|
|
flattened = self.flatten(path)
|
|
|
|
# manipulate invalid path
|
|
if len(flattened[0]) == 1:
|
|
return [[[flattened[0][0][0], flattened[0][0][1]], [flattened[0][0][0] + 1.0, flattened[0][0][1]]]]
|
|
|
|
if self.manual_stitch_mode:
|
|
return [self.strip_control_points(subpath) for subpath in path]
|
|
else:
|
|
return flattened
|
|
|
|
@property
|
|
@cache
|
|
def shape(self):
|
|
return self.as_multi_line_string().convex_hull
|
|
|
|
@cache
|
|
def as_multi_line_string(self):
|
|
line_strings = [shapely.geometry.LineString(path) for path in self.paths]
|
|
return shapely.geometry.MultiLineString(line_strings)
|
|
|
|
def get_ripple_target(self):
|
|
command = self.get_command('ripple_target')
|
|
if command:
|
|
pos = [float(command.use.get("x", 0)), float(command.use.get("y", 0))]
|
|
transform = get_node_transform(command.use)
|
|
pos = Transform(transform).apply_to_point(pos)
|
|
return Point(*pos)
|
|
else:
|
|
return self.shape.centroid
|
|
|
|
def is_running_stitch(self):
|
|
# using stroke width <= 0.5 pixels to indicate running stitch is deprecated in favor of dashed lines
|
|
|
|
stroke_width, units = parse_length_with_units(self.get_style("stroke-width", "1"))
|
|
|
|
if self.dashed:
|
|
return True
|
|
elif stroke_width <= 0.5 and self.get_float_param('running_stitch_length_mm', None) is not None:
|
|
# if they use a stroke width less than 0.5 AND they specifically set a running stitch
|
|
# length, then assume they intend to use the deprecated <= 0.5 method to set running
|
|
# stitch.
|
|
#
|
|
# Note that we use self.get_style("stroke_width") _not_ self.stroke_width above. We
|
|
# explicitly want the stroke width in "user units" ("document units") -- that is, what
|
|
# the user sees in inkscape's stroke settings.
|
|
#
|
|
# Also note that we don't use self.running_stitch_length_mm above. This is because we
|
|
# want to see if they set a running stitch length at all, and the property will apply
|
|
# a default value.
|
|
#
|
|
# This is so tricky, and and intricate that's a major reason that we deprecated the
|
|
# 0.5 units rule.
|
|
|
|
# Warn them the first time.
|
|
global warned_about_legacy_running_stitch
|
|
if not warned_about_legacy_running_stitch:
|
|
warned_about_legacy_running_stitch = True
|
|
print(_("Legacy running stitch setting detected!\n\nIt looks like you're using a stroke " +
|
|
"smaller than 0.5 units to indicate a running stitch, which is deprecated. Instead, please set " +
|
|
"your stroke to be dashed to indicate running stitch. Any kind of dash will work."), file=sys.stderr)
|
|
|
|
# still allow the deprecated setting to work in order to support old files
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
def simple_satin(self, path, zigzag_spacing, stroke_width):
|
|
"zig-zag along the path at the specified spacing and wdith"
|
|
|
|
# `self.zigzag_spacing` is the length for a zig and a zag
|
|
# together (a V shape). Start with running stitch at half
|
|
# that length:
|
|
patch = self.running_stitch(path, zigzag_spacing / 2.0, self.running_stitch_tolerance)
|
|
|
|
# Now move the points left and right. Consider each pair
|
|
# of points in turn, and move perpendicular to them,
|
|
# alternating left and right.
|
|
|
|
offset = stroke_width / 2.0
|
|
|
|
for i in range(len(patch) - 1):
|
|
start = patch.stitches[i]
|
|
end = patch.stitches[i + 1]
|
|
# sometimes the stitch results into zero length which cause a division by zero error
|
|
# ignoring this leads to a slightly bad result, but that is better than no output
|
|
if (end - start).length() == 0:
|
|
continue
|
|
segment_direction = (end - start).unit()
|
|
zigzag_direction = segment_direction.rotate_left()
|
|
|
|
if i % 2 == 1:
|
|
zigzag_direction *= -1
|
|
|
|
patch.stitches[i] += zigzag_direction * offset
|
|
|
|
return patch
|
|
|
|
def running_stitch(self, path, stitch_length, tolerance):
|
|
repeated_path = []
|
|
|
|
# go back and forth along the path as specified by self.repeats
|
|
for i in range(self.repeats):
|
|
if i % 2 == 1:
|
|
# reverse every other pass
|
|
this_path = path[::-1]
|
|
else:
|
|
this_path = path[:]
|
|
|
|
repeated_path.extend(this_path)
|
|
|
|
stitches = running_stitch(repeated_path, stitch_length, tolerance)
|
|
|
|
return StitchGroup(self.color, stitches)
|
|
|
|
def ripple_stitch(self):
|
|
return StitchGroup(
|
|
color=self.color,
|
|
tags=["ripple_stitch"],
|
|
stitches=ripple_stitch(self))
|
|
|
|
def do_bean_repeats(self, stitches):
|
|
return bean_stitch(stitches, self.bean_stitch_repeats)
|
|
|
|
def to_stitch_groups(self, last_patch):
|
|
patches = []
|
|
|
|
# ripple stitch
|
|
if self.stroke_method == 1:
|
|
patch = self.ripple_stitch()
|
|
if patch:
|
|
if self.bean_stitch_repeats > 0:
|
|
patch.stitches = self.do_bean_repeats(patch.stitches)
|
|
patches.append(patch)
|
|
else:
|
|
for path in self.paths:
|
|
path = [Point(x, y) for x, y in path]
|
|
# manual stitch
|
|
if self.manual_stitch_mode:
|
|
patch = StitchGroup(color=self.color, stitches=path, stitch_as_is=True)
|
|
# running stitch
|
|
elif self.is_running_stitch():
|
|
patch = self.running_stitch(path, self.running_stitch_length, self.running_stitch_tolerance)
|
|
if self.bean_stitch_repeats > 0:
|
|
patch.stitches = self.do_bean_repeats(patch.stitches)
|
|
# simple satin
|
|
else:
|
|
patch = self.simple_satin(path, self.zigzag_spacing, self.stroke_width)
|
|
|
|
if patch:
|
|
patches.append(patch)
|
|
|
|
return patches
|
|
|
|
@cache
|
|
def get_guide_line(self):
|
|
guide_lines = get_marker_elements(self.node, "guide-line", False, True, True)
|
|
# No or empty guide line
|
|
# if there is a satin guide line, it will also be in stroke, so no need to check for satin here
|
|
if not guide_lines or not guide_lines['stroke']:
|
|
return None
|
|
|
|
# use the satin guide line if there is one, else use stroke
|
|
# ignore multiple guide lines
|
|
if len(guide_lines['satin']) >= 1:
|
|
return guide_lines['satin'][0]
|
|
return guide_lines['stroke'][0]
|
|
|
|
def _representative_point(self):
|
|
# if we just take the center of a line string we could end up on some point far away from the actual line
|
|
try:
|
|
coords = list(self.shape.coords)
|
|
except NotImplementedError:
|
|
# linear rings to not have a coordinate sequence
|
|
coords = list(self.shape.exterior.coords)
|
|
return coords[int(len(coords)/2)]
|
|
|
|
def validation_warnings(self):
|
|
if self.stroke_method == 1 and self.skip_start + self.skip_end >= self.line_count:
|
|
yield IgnoreSkipValues(self.shape.centroid)
|
|
|
|
# guided fill warnings
|
|
if self.stroke_method == 1:
|
|
guide_lines = get_marker_elements(self.node, "guide-line", False, True, True)
|
|
if sum(len(x) for x in guide_lines.values()) > 1:
|
|
yield MultipleGuideLineWarning(self._representative_point())
|
|
|
|
stroke_width, units = parse_length_with_units(self.get_style("stroke-width", "1"))
|
|
if not self.dashed and stroke_width <= 0.5:
|
|
yield SmallZigZagWarning(self._representative_point())
|