summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKaalleen <36401965+kaalleen@users.noreply.github.com>2023-02-27 16:05:52 +0100
committerGitHub <noreply@github.com>2023-02-27 16:05:52 +0100
commitec076315bb8b5f901670fee1c06db028242b21fd (patch)
tree17cd3413f48cad338d95452d2d0aac96aac44d01
parented4aa55a733986436853e2ee7ad22f757b09fcb1 (diff)
Various lock stitch options (#2006)
Co-authored-by: Lex Neva
-rw-r--r--lib/elements/element.py159
-rw-r--r--lib/elements/fill_stitch.py14
-rw-r--r--lib/elements/polyline.py6
-rw-r--r--lib/elements/satin_column.py12
-rw-r--r--lib/elements/stroke.py13
-rw-r--r--lib/stitch_plan/lock_stitch.py224
-rw-r--r--lib/stitch_plan/stitch.py30
-rw-r--r--lib/stitch_plan/stitch_group.py21
-rw-r--r--lib/stitch_plan/stitch_plan.py56
-rw-r--r--lib/stitch_plan/ties.py73
-rw-r--r--lib/stitches/fill.py2
-rw-r--r--lib/svg/tags.py11
-rw-r--r--lib/utils/string.py10
13 files changed, 476 insertions, 155 deletions
diff --git a/lib/elements/element.py b/lib/elements/element.py
index 3a9f331c..ba15d943 100644
--- a/lib/elements/element.py
+++ b/lib/elements/element.py
@@ -4,9 +4,9 @@
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
import sys
from copy import deepcopy
-import numpy as np
import inkex
+import numpy as np
from inkex import bezier
from ..commands import find_commands
@@ -14,6 +14,8 @@ from ..debug import debug
from ..i18n import _
from ..marker import get_marker_elements_cache_key_data
from ..patterns import apply_patterns, get_patterns_cache_key_data
+from ..stitch_plan.lock_stitch import (LOCK_DEFAULTS, AbsoluteLock, CustomLock,
+ LockStitch, SVGLock)
from ..svg import (PIXELS_PER_MM, apply_transforms, convert_length,
get_node_transform)
from ..svg.tags import INKSCAPE_LABEL, INKSTITCH_ATTRIBS
@@ -262,6 +264,119 @@ class EmbroideryElement(object):
return self.get_boolean_param('force_lock_stitches', False)
@property
+ @param('lock_start',
+ _('Tack stitch'),
+ tooltip=_('Tack down stitch type'),
+ type='combo',
+ default='half_stitch',
+ options=LOCK_DEFAULTS['start'],
+ sort_index=52)
+ def lock_start(self):
+ return self.get_param('lock_start', "half_stitch")
+
+ @property
+ @param('lock_custom_start',
+ _('Custom path'),
+ tooltip=_("Enter a custom path. For svg paths The last node will not be embroidered, but represents the first stitch of the element."),
+ type="string",
+ default="",
+ select_items=[('lock_start', 'custom')],
+ sort_index=53)
+ def lock_custom_start(self):
+ return self.get_param('lock_custom_start', '')
+
+ @property
+ @param('lock_start_scale_mm',
+ _('Scale tack stitch'),
+ tooltip=_('Set stitch length. A 1 in a custom path equals this values.'),
+ type='float',
+ unit="mm",
+ default=0.7,
+ select_items=[('lock_start', lock.id) for lock in LOCK_DEFAULTS['start'] if isinstance(lock, (AbsoluteLock, CustomLock))],
+ sort_index=54)
+ def lock_start_scale_mm(self):
+ return self.get_float_param('lock_start_scale_mm', 0.7)
+
+ @property
+ @param('lock_start_scale_percent',
+ _('Scale tack stitch'),
+ tooltip=_('Scale tack stitch by this percentage.'),
+ type='float',
+ unit="%",
+ default=100,
+ select_items=[('lock_start', lock.id) for lock in LOCK_DEFAULTS['start'] if isinstance(lock, (SVGLock, CustomLock))],
+ sort_index=54)
+ def lock_start_scale_percent(self):
+ return self.get_float_param('lock_start_scale_percent', 100)
+
+ @property
+ @param('lock_end',
+ _('Lock stitch'),
+ tooltip=_('Lock stitch type'),
+ type='combo',
+ default='half_stitch',
+ options=LOCK_DEFAULTS['end'],
+ sort_index=55)
+ def lock_end(self):
+ return self.get_param('lock_end', "half_stitch")
+
+ @property
+ @param('lock_custom_end',
+ _('Custom path'),
+ tooltip=_("Enter a custom path. For svg paths the first node will not be embroidered, but represents the last stitch of the element."),
+ type="string",
+ default="",
+ select_items=[('lock_end', 'custom')],
+ sort_index=56)
+ def lock_custom_end(self):
+ return self.get_param('lock_custom_end', '')
+
+ @property
+ @param('lock_end_scale_mm',
+ _('Scale lock stitch'),
+ tooltip=_('Set length of lock stitches (mm).'),
+ type='float',
+ unit="mm",
+ default=0.7,
+ select_items=[('lock_end', lock.id) for lock in LOCK_DEFAULTS['end'] if isinstance(lock, (AbsoluteLock, CustomLock))],
+ sort_index=57)
+ def lock_end_scale_mm(self):
+ return self.get_float_param('lock_end_scale_mm', 0.7)
+
+ @property
+ @param('lock_end_scale_percent',
+ _('Scale lock stitch'),
+ tooltip=_('Scale lock stitch by this percentage.'),
+ type='float',
+ unit="%",
+ default=100,
+ select_items=[('lock_end', lock.id) for lock in LOCK_DEFAULTS['end'] if isinstance(lock, (SVGLock, CustomLock))],
+ sort_index=57)
+ @cache
+ def lock_end_scale_percent(self):
+ return self.get_float_param('lock_end_scale_percent', 100)
+
+ @property
+ @param('trim_after',
+ _('Trim After'),
+ tooltip=_('Add a TRIM command after stitching this object.'),
+ type='boolean',
+ default=False,
+ sort_index=60)
+ def trim_after(self):
+ return self.get_boolean_param('trim_after', False)
+
+ @property
+ @param('stop_after',
+ _('Stop After'),
+ tooltip=_('Add a STOP command after stitching this object.'),
+ type='boolean',
+ default=False,
+ sort_index=60)
+ def stop_after(self):
+ return self.get_boolean_param('stop_after', False)
+
+ @property
@param('random_seed',
_('Random seed'),
tooltip=_('Use a specific seed for randomized attributes. Uses the element ID if empty.'),
@@ -314,7 +429,7 @@ class EmbroideryElement(object):
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 inkex.paths.Path(d).to_superpath()
+ return inkex.Path(d).to_superpath()
@cache
def parse_path(self):
@@ -369,24 +484,26 @@ class EmbroideryElement(object):
return self.strip_control_points(path[0])
@property
- @param('trim_after',
- _('Trim After'),
- tooltip=_('Add a TRIM command after stitching this object.'),
- type='boolean',
- default=False,
- sort_index=52)
- def trim_after(self):
- return self.get_boolean_param('trim_after', False)
+ @cache
+ def lock_stitches(self):
+ lock_start = None
+ lock_end = None
- @property
- @param('stop_after',
- _('Stop After'),
- tooltip=_('Add a STOP command after stitching this object.'),
- type='boolean',
- default=False,
- sort_index=53)
- def stop_after(self):
- return self.get_boolean_param('stop_after', False)
+ # Ties: 0 = Both | 1 = Before | 2 = After | 3 = Neither
+ tie_modus = self.ties
+ force = self.force_lock_stitches
+
+ if tie_modus in [0, 1]:
+ lock_start = LockStitch('start', self.lock_start, scale_percent=self.lock_start_scale_percent, scale_absolute=self.lock_start_scale_mm)
+ if self.lock_start == "custom":
+ lock_start.path = self.lock_custom_start
+
+ if tie_modus in [0, 2] or force:
+ lock_end = LockStitch('end', self.lock_end, scale_percent=self.lock_end_scale_percent, scale_absolute=self.lock_end_scale_mm)
+ if self.lock_end == "custom":
+ lock_end.path = self.lock_custom_end
+
+ return lock_start, lock_end
def to_stitch_groups(self, last_patch):
raise NotImplementedError("%s must implement to_stitch_groups()" % self.__class__.__name__)
@@ -477,10 +594,6 @@ class EmbroideryElement(object):
stitch_groups = self.to_stitch_groups(last_stitch_group)
apply_patterns(stitch_groups, self.node)
- for stitch_group in stitch_groups:
- stitch_group.tie_modus = self.ties
- stitch_group.force_lock_stitches = self.force_lock_stitches
-
if stitch_groups:
stitch_groups[-1].trim_after = self.has_command("trim") or self.trim_after
stitch_groups[-1].stop_after = self.has_command("stop") or self.stop_after
diff --git a/lib/elements/fill_stitch.py b/lib/elements/fill_stitch.py
index 1897c8bf..8eb12af2 100644
--- a/lib/elements/fill_stitch.py
+++ b/lib/elements/fill_stitch.py
@@ -673,7 +673,10 @@ class FillStitch(EmbroideryElement):
self.flip,
self.staggers,
self.skip_last)
- return [StitchGroup(stitches=stitch_list, color=self.color) for stitch_list in stitch_lists]
+ return [StitchGroup(stitches=stitch_list,
+ color=self.color,
+ force_lock_stitches=self.force_lock_stitches,
+ lock_stitches=self.lock_stitches) for stitch_list in stitch_lists]
def do_underlay(self, shape, starting_point):
stitch_groups = []
@@ -681,6 +684,7 @@ class FillStitch(EmbroideryElement):
underlay = StitchGroup(
color=self.color,
tags=("auto_fill", "auto_fill_underlay"),
+ lock_stitches=self.lock_stitches,
stitches=auto_fill(
shape,
self.fill_underlay_angle[i],
@@ -702,6 +706,8 @@ class FillStitch(EmbroideryElement):
stitch_group = StitchGroup(
color=self.color,
tags=("auto_fill", "auto_fill_top"),
+ force_lock_stitches=self.force_lock_stitches,
+ lock_stitches=self.lock_stitches,
stitches=auto_fill(
shape,
self.angle,
@@ -755,7 +761,9 @@ class FillStitch(EmbroideryElement):
stitch_group = StitchGroup(
color=self.color,
tags=("auto_fill", "auto_fill_top"),
- stitches=stitches)
+ stitches=stitches,
+ force_lock_stitches=self.force_lock_stitches,
+ lock_stitches=self.lock_stitches,)
stitch_groups.append(stitch_group)
return stitch_groups
@@ -770,6 +778,8 @@ class FillStitch(EmbroideryElement):
stitch_group = StitchGroup(
color=self.color,
tags=("guided_fill", "auto_fill_top"),
+ force_lock_stitches=self.force_lock_stitches,
+ lock_stitches=self.lock_stitches,
stitches=guided_fill(
shape,
guide_line.geoms[0],
diff --git a/lib/elements/polyline.py b/lib/elements/polyline.py
index 5086c705..a33b75de 100644
--- a/lib/elements/polyline.py
+++ b/lib/elements/polyline.py
@@ -6,12 +6,12 @@
from inkex import Path
from shapely import geometry as shgeo
-from .element import EmbroideryElement, param
-from .validation import ValidationWarning
from ..i18n import _
from ..stitch_plan import StitchGroup
from ..utils import cache
from ..utils.geometry import Point
+from .element import EmbroideryElement, param
+from .validation import ValidationWarning
class PolylineWarning(ValidationWarning):
@@ -95,7 +95,7 @@ class Polyline(EmbroideryElement):
yield PolylineWarning(self.path[0][0][0])
def to_stitch_groups(self, last_patch):
- patch = StitchGroup(color=self.color)
+ patch = StitchGroup(color=self.color, lock_stitches=(None, None))
for stitch in self.stitches:
patch.add_stitch(Point(*stitch))
diff --git a/lib/elements/satin_column.py b/lib/elements/satin_column.py
index eba63c6c..b5086171 100644
--- a/lib/elements/satin_column.py
+++ b/lib/elements/satin_column.py
@@ -3,10 +3,10 @@
# Copyright (c) 2010 Authors
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
-from copy import deepcopy
import itertools
-from itertools import chain
import typing
+from copy import deepcopy
+from itertools import chain
import numpy as np
from inkex import paths
@@ -16,10 +16,10 @@ from shapely.ops import nearest_points
from ..i18n import _
from ..stitch_plan import StitchGroup
+from ..stitches import running_stitch
from ..svg import line_strings_to_csp, point_lists_to_csp
from ..utils import Point, cache, cut, cut_multiple, prng
-from ..stitches import running_stitch
-from .element import EmbroideryElement, param, PIXELS_PER_MM
+from .element import PIXELS_PER_MM, EmbroideryElement, param
from .validation import ValidationError, ValidationWarning
from ..utils.threading import check_stop_flag
@@ -1092,7 +1092,9 @@ class SatinColumn(EmbroideryElement):
# beziers. The boundary points between beziers serve as "checkpoints",
# allowing the user to control how the zigzags flow around corners.
- patch = StitchGroup(color=self.color)
+ patch = StitchGroup(color=self.color,
+ force_lock_stitches=self.force_lock_stitches,
+ lock_stitches=self.lock_stitches)
if self.center_walk_underlay:
patch += self.do_center_walk()
diff --git a/lib/elements/stroke.py b/lib/elements/stroke.py
index 6aca3847..c633a2c6 100644
--- a/lib/elements/stroke.py
+++ b/lib/elements/stroke.py
@@ -6,19 +6,18 @@
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.running_stitch import bean_stitch, running_stitch
from ..stitches.ripple_stitch import ripple_stitch
+from ..stitches.running_stitch import bean_stitch, running_stitch
from ..svg import get_node_transform, parse_length_with_units
+from ..threads import ThreadColor
from ..utils import Point, cache
from .element import EmbroideryElement, param
from .validation import ValidationWarning
-from ..threads import ThreadColor
warned_about_legacy_running_stitch = False
@@ -445,13 +444,15 @@ class Stroke(EmbroideryElement):
repeated_stitches.extend(this_path)
- return StitchGroup(self.color, repeated_stitches)
+ return StitchGroup(self.color, repeated_stitches, lock_stitches=self.lock_stitches, force_lock_stitches=self.force_lock_stitches)
def ripple_stitch(self):
return StitchGroup(
color=self.color,
tags=["ripple_stitch"],
- stitches=ripple_stitch(self))
+ stitches=ripple_stitch(self),
+ lock_stitches=self.lock_stitches,
+ force_lock_stitches=self.force_lock_stitches)
def do_bean_repeats(self, stitches):
return bean_stitch(stitches, self.bean_stitch_repeats)
@@ -471,7 +472,7 @@ class Stroke(EmbroideryElement):
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)
+ patch = StitchGroup(color=self.color, stitches=path, lock_stitches=(None, None))
# running stitch
elif self.is_running_stitch():
patch = self.running_stitch(path, self.running_stitch_length, self.running_stitch_tolerance)
diff --git a/lib/stitch_plan/lock_stitch.py b/lib/stitch_plan/lock_stitch.py
new file mode 100644
index 00000000..ba0e5ba7
--- /dev/null
+++ b/lib/stitch_plan/lock_stitch.py
@@ -0,0 +1,224 @@
+import re
+from copy import copy
+from math import degrees
+
+from inkex import DirectedLineSegment, Path
+from shapely.geometry import LineString
+from shapely.ops import substring
+
+from ..i18n import _
+from ..svg import PIXELS_PER_MM
+from ..utils import string_to_floats
+from .stitch import Stitch
+
+
+class LockStitchDefinition:
+ def __init__(self, lock_id=None, name=None, path=None):
+ self.id: str = lock_id
+ self.name: str = name
+ self._path: str = path
+
+ def __repr__(self):
+ return "LockStitchDefinition(%s, %s, %s)" % (self.id, self.name, self.path)
+
+ def stitches(self):
+ raise NotImplementedError(f"{self.__class__.__name__} must implement stitches()")
+
+
+class LockStitch:
+ def __init__(self, lock_type, lock_id, scale_percent, scale_absolute):
+ self.lock_stitch_definition = get_lock_stitch_definition_by_id(lock_type, lock_id)
+ self.scale = LockStitchScale(scale_percent, scale_absolute)
+
+ def stitches(self, stitches, pos):
+ return self.lock_stitch_definition.stitches(stitches, pos, self.scale)
+
+
+class LockStitchScale:
+ def __init__(self, scale_percent, scale_absolute):
+ self.percent = scale_percent / 100
+ self.absolute = scale_absolute
+
+
+class CustomLock(LockStitchDefinition):
+ @property
+ def path(self):
+ return self._path
+
+ @path.setter
+ def path(self, path):
+ path_type = self._get_path_type(path)
+ if path_type in ['svg', 'absolute']:
+ self._path = path
+ else:
+ self._path = None
+
+ def stitches(self, stitches, pos, scale):
+ if self.path is None:
+ return half_stitch.stitches(stitches, pos)
+
+ path_type = self._get_path_type(self.path)
+ if path_type == "svg":
+ return SVGLock(self.id,
+ self.name,
+ self.path).stitches(stitches, pos, scale.percent)
+ else:
+ return AbsoluteLock(self.id,
+ self.name,
+ self.path).stitches(stitches, pos, scale.absolute)
+
+ def _get_path_type(self, path):
+ if not path:
+ return "invalid"
+ if not re.match("^ *[0-9 .,-]*$", path):
+ path = Path(path)
+ if not path or len(list(path.end_points)) < 3:
+ return None
+ else:
+ return "svg"
+ else:
+ path = string_to_floats(path, " ")
+ if not path:
+ return "invalid"
+ else:
+ return "absolute"
+
+
+class RelativeLock(LockStitchDefinition):
+ def stitches(self, stitches, pos, scale):
+ if pos == "end":
+ stitches = list(reversed(stitches))
+
+ path = string_to_floats(self._path, " ")
+
+ to_previous = stitches[1] - stitches[0]
+ length = to_previous.length()
+
+ lock_stitches = []
+ if length > 0.5 * PIXELS_PER_MM:
+
+ # Travel back one stitch, stopping halfway there.
+ # Then go forward one stitch, stopping halfway between
+ # again.
+
+ # but travel at most 1.5 mm
+ length = min(length, 1.5 * PIXELS_PER_MM)
+
+ direction = to_previous.unit()
+
+ for delta in path:
+ lock_stitches.append(Stitch(stitches[0] + delta * length * direction, tags=('lock_stitch')))
+ else:
+ # Too short to travel part of the way to the previous stitch; just go
+ # back and forth to it a couple times.
+ for i in (1, 0, 1, 0):
+ lock_stitches.append(stitches[i])
+ return lock_stitches
+
+
+class AbsoluteLock(LockStitchDefinition):
+ def stitches(self, stitches, pos, scale):
+ if pos == "end":
+ stitches = list(reversed(stitches))
+
+ # make sure the path consists of only floats
+ path = string_to_floats(self._path, " ")
+
+ # get the length of our lock stitch path
+ if pos == 'start':
+ lock_pos = []
+ lock = 0
+ # reverse the list to make sure we end with the first stitch of the target path
+ for tie_path in reversed(path):
+ lock = lock - tie_path * scale.absolute
+ lock_pos.insert(0, lock)
+ elif pos == 'end':
+ lock_pos = []
+ lock = 0
+ for tie_path in path:
+ lock = lock + tie_path * scale.absolute
+ lock_pos.append(lock)
+ max_lock_length = max(lock_pos)
+
+ # calculate the amount stitches we need from the target path
+ # and generate a line
+ upcoming = [stitches[0]]
+ for stitch in stitches[1:]:
+ to_start = stitch - upcoming[-1]
+ upcoming.append(stitch)
+ if to_start.length() >= max_lock_length:
+ break
+ line = LineString(upcoming)
+
+ # add tie stitches
+ lock_stitches = []
+ for i, tie_path in enumerate(lock_pos):
+ if tie_path < 0:
+ stitch = Stitch(stitches[0] + tie_path * (stitches[1] - stitches[0]).unit())
+ else:
+ point = line.interpolate(tie_path)
+ stitch = Stitch(point.x, point.y, tags=('lock_stitch',))
+ lock_stitches.append(stitch)
+ return lock_stitches
+
+
+class SVGLock(LockStitchDefinition):
+ def stitches(self, stitches, pos, scale):
+ if pos == "end":
+ stitches = list(reversed(stitches))
+
+ path = Path(self._path)
+ path.scale(PIXELS_PER_MM, PIXELS_PER_MM, True)
+ path.scale(scale.percent, scale.percent, True)
+
+ end_points = list(path.end_points)
+
+ lock = DirectedLineSegment(end_points[-2], end_points[-1])
+ lock_stitch_angle = lock.angle
+
+ stitch = DirectedLineSegment((stitches[0].x, stitches[0].y),
+ (stitches[1].x, stitches[1].y))
+ stitch_angle = stitch.angle
+
+ # rotate and translate the lock stitch
+ path.rotate(degrees(stitch_angle - lock_stitch_angle), lock.start, True)
+ translate = stitch.start - lock.start
+ path.translate(translate.x, translate.y, True)
+
+ # Remove direction indicator from path and also
+ # remove start:last/end:first stitch (it is the position of the first/last stitch of the target path)
+ path = list(path.end_points)[:-2]
+
+ if pos == 'end':
+ path = reversed(path)
+
+ lock_stitches = []
+ for i, stitch in enumerate(path):
+ stitch = Stitch(stitch[0], stitch[1], tags=('lock_stitch',))
+ lock_stitches.append(stitch)
+ return lock_stitches
+
+
+def get_lock_stitch_definition_by_id(pos, lock_type, default="half_stitch"):
+ id_list = [lock.id for lock in LOCK_DEFAULTS[pos]]
+
+ try:
+ lock = LOCK_DEFAULTS[pos][id_list.index(lock_type)]
+ except ValueError:
+ lock = LOCK_DEFAULTS[pos][id_list.index(default)]
+ return lock
+
+
+half_stitch = RelativeLock("half_stitch", _("Half Stitch"), "0 0.5 1 0.5 0")
+arrow = SVGLock("arrow", _("Arrow"), "M 0.5,0.3 0.3,1.31 -0.11,0.68 H 0.9 L 0.5,1.31 0.4,0.31 V 0.31 1.3")
+back_forth = AbsoluteLock("back_forth", _("Back and forth"), "1 1 -1 -1")
+bowtie = SVGLock("bowtie", _("Bowtie"), "M 0,0 -0.39,0.97 0.3,0.03 0.14,1.02 0,0 V 0.15")
+cross = SVGLock("cross", _("Cross"), "M 0,0 -0.7,-0.7 0.7,0.7 0,0 -0.7,0.7 0.7,-0.7 0,0 -0,-0.7")
+star = SVGLock("star", _("Star"), "M 0.67,-0.2 C 0.27,-0.06 -0.22,0.11 -0.67,0.27 L 0.57,0.33 -0.5,-0.27 0,0.67 V 0 -0.5")
+simple = SVGLock("simple", _("Simple"), "M -0.03,0 0.09,0.81 0,1.49 V 0 0.48")
+triangle = SVGLock("triangle", _("Triangle"), "M -0.26,0.33 H 0.55 L 0,0.84 V 0 L 0.34,0.82")
+zigzag = SVGLock("zigzag", _("Zig-zag"), "M -0.25,0.2 0.17,0.77 -0.22,1.45 0.21,2.05 -0.03,3 0,0")
+custom = CustomLock("custom", _("Custom"))
+
+LOCK_DEFAULTS = {'start': [half_stitch, arrow, back_forth, bowtie, cross, star, simple, triangle, zigzag, custom],
+ 'end': [half_stitch, arrow, back_forth, cross, bowtie, star, simple, triangle, zigzag, custom]}
diff --git a/lib/stitch_plan/stitch.py b/lib/stitch_plan/stitch.py
index c1553ce5..90af58c0 100644
--- a/lib/stitch_plan/stitch.py
+++ b/lib/stitch_plan/stitch.py
@@ -3,15 +3,15 @@
# Copyright (c) 2010 Authors
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
-from ..utils.geometry import Point
from shapely import geometry as shgeo
+from ..utils.geometry import Point
+
class Stitch(Point):
"""A stitch is a Point with extra information telling how to sew it."""
- def __init__(self, x, y=None, color=None, jump=False, stop=False, trim=False, color_change=False,
- tie_modus=0, force_lock_stitches=False, no_ties=False, tags=None):
+ def __init__(self, x, y=None, color=None, jump=False, stop=False, trim=False, color_change=False, tags=None):
# DANGER: if you add new attributes, you MUST also set their default
# values in __new__() below. Otherwise, cached stitch plans can be
# loaded and create objects without those properties defined, because
@@ -37,9 +37,6 @@ class Stitch(Point):
self._set('trim', trim, base_stitch)
self._set('stop', stop, base_stitch)
self._set('color_change', color_change, base_stitch)
- self._set('force_lock_stitches', force_lock_stitches, base_stitch)
- self._set('tie_modus', tie_modus, base_stitch)
- self._set('no_ties', no_ties, base_stitch)
self.tags = set()
self.add_tags(tags or [])
@@ -55,17 +52,13 @@ class Stitch(Point):
return instance
def __repr__(self):
- return "Stitch(%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)" % (self.x,
- self.y,
- self.color,
- self.tags,
- "JUMP" if self.jump else " ",
- "TRIM" if self.trim else " ",
- "STOP" if self.stop else " ",
- self.tie_modus,
- "FORCE LOCK STITCHES" if self.force_lock_stitches else " ",
- "NO TIES" if self.no_ties else " ",
- "COLOR CHANGE" if self.color_change else " ")
+ return "Stitch(%s, %s, %s, %s, %s, %s, %s)" % (self.x,
+ self.y,
+ self.color,
+ "JUMP" if self.jump else " ",
+ "TRIM" if self.trim else " ",
+ "STOP" if self.stop else " ",
+ "COLOR CHANGE" if self.color_change else " ")
def _set(self, attribute, value, base_stitch):
# Set an attribute. If the caller passed a Stitch object, use its value, unless
@@ -98,8 +91,7 @@ class Stitch(Point):
return tag in self.tags
def copy(self):
- return Stitch(self.x, self.y, self.color, self.jump, self.stop, self.trim, self.color_change,
- self.tie_modus, self.force_lock_stitches, self.no_ties, self.tags)
+ return Stitch(self.x, self.y, self.color, self.jump, self.stop, self.trim, self.color_change, self.tags)
def __json__(self):
attributes = dict(vars(self))
diff --git a/lib/stitch_plan/stitch_group.py b/lib/stitch_plan/stitch_group.py
index 717bb617..957da3f8 100644
--- a/lib/stitch_plan/stitch_group.py
+++ b/lib/stitch_plan/stitch_group.py
@@ -18,7 +18,7 @@ class StitchGroup:
"""
def __init__(self, color=None, stitches=None, trim_after=False, stop_after=False,
- tie_modus=0, force_lock_stitches=False, stitch_as_is=False, tags=None):
+ lock_stitches=(None, None), force_lock_stitches=False, tags=None):
# DANGER: if you add new attributes, you MUST also set their default
# values in __new__() below. Otherwise, cached stitch plans can be
# loaded and create objects without those properties defined, because
@@ -27,9 +27,8 @@ class StitchGroup:
self.color = color
self.trim_after = trim_after
self.stop_after = stop_after
- self.tie_modus = tie_modus
+ self.lock_stitches = lock_stitches
self.force_lock_stitches = force_lock_stitches
- self.stitch_as_is = stitch_as_is
self.stitches = []
if stitches:
@@ -44,11 +43,14 @@ class StitchGroup:
# Set default values for any new attributes here (see note in __init__() above)
# instance.foo = None
+ instance.lock_stitches = None
+
return instance
def __add__(self, other):
if isinstance(other, StitchGroup):
- return StitchGroup(self.color, self.stitches + other.stitches)
+ return StitchGroup(self.color, self.stitches + other.stitches,
+ lock_stitches=self.lock_stitches, force_lock_stitches=self.force_lock_stitches)
else:
raise TypeError("StitchGroup can only be added to another StitchGroup")
@@ -77,3 +79,14 @@ class StitchGroup:
def add_tag(self, tag):
for stitch in self.stitches:
stitch.add_tag(tag)
+
+ def get_lock_stitches(self, pos, disable_ties=False):
+ if len(self.stitches) < 2:
+ return []
+
+ lock_pos = 0 if pos == "start" else 1
+ if disable_ties or self.lock_stitches[lock_pos] is None:
+ return
+
+ stitches = self.lock_stitches[lock_pos].stitches(self.stitches, pos)
+ return stitches
diff --git a/lib/stitch_plan/stitch_plan.py b/lib/stitch_plan/stitch_plan.py
index 741ec006..1a846099 100644
--- a/lib/stitch_plan/stitch_plan.py
+++ b/lib/stitch_plan/stitch_plan.py
@@ -10,7 +10,6 @@ from inkex import errormsg
from ..i18n import _
from ..svg import PIXELS_PER_MM
from .color_block import ColorBlock
-from .ties import add_ties
from ..utils.threading import check_stop_flag
@@ -31,9 +30,13 @@ def stitch_groups_to_stitch_plan(stitch_groups, collapse_len=None, min_stitch_le
if collapse_len is None:
collapse_len = 3.0
collapse_len = collapse_len * PIXELS_PER_MM
+
stitch_plan = StitchPlan()
color_block = stitch_plan.new_color_block(color=stitch_groups[0].color)
+ previous_stitch_group = None
+ need_tie_in = True
+
for stitch_group in stitch_groups:
check_stop_flag()
@@ -49,22 +52,42 @@ def stitch_groups_to_stitch_plan(stitch_groups, collapse_len=None, min_stitch_le
# We'll just claim this new block as ours:
color_block.color = stitch_group.color
else:
+ # add a lock stitch to the last element of the previous group
+ lock_stitches = previous_stitch_group.get_lock_stitches("end", disable_ties)
+ if lock_stitches:
+ color_block.add_stitches(stitches=lock_stitches)
+ need_tie_in = True
+
# end the previous block with a color change
color_block.add_stitch(color_change=True)
# make a new block of our color
color_block = stitch_plan.new_color_block(color=stitch_group.color)
-
- # always start a color with a JUMP to the first stitch position
- color_block.add_stitch(stitch_group.stitches[0], jump=True, tie_modus=stitch_group.tie_modus)
else:
- if (len(color_block) and
+ if (len(color_block) and not need_tie_in and
((stitch_group.stitches[0] - color_block.stitches[-1]).length() > collapse_len or
- color_block.stitches[-1].force_lock_stitches)):
- color_block.add_stitch(stitch_group.stitches[0], jump=True, tie_modus=stitch_group.tie_modus)
+ previous_stitch_group.force_lock_stitches)):
+ lock_stitches = previous_stitch_group.get_lock_stitches("end", disable_ties)
+ if lock_stitches:
+ color_block.add_stitches(stitches=lock_stitches)
+ need_tie_in = True
+
+ if need_tie_in is True:
+ lock_stitches = stitch_group.get_lock_stitches("start", disable_ties)
+ if lock_stitches:
+ color_block.add_stitch(lock_stitches[0], jump=True)
+ color_block.add_stitches(stitches=lock_stitches)
+ else:
+ color_block.add_stitch(stitch_group.stitches[0], jump=True)
+ need_tie_in = False
- color_block.add_stitches(stitches=stitch_group.stitches, tie_modus=stitch_group.tie_modus,
- force_lock_stitches=stitch_group.force_lock_stitches, no_ties=stitch_group.stitch_as_is)
+ color_block.add_stitches(stitches=stitch_group.stitches)
+
+ if stitch_group.trim_after or stitch_group.stop_after:
+ lock_stitches = stitch_group.get_lock_stitches("end", disable_ties)
+ if lock_stitches:
+ color_block.add_stitches(stitches=lock_stitches)
+ need_tie_in = True
if stitch_group.trim_after:
color_block.add_stitch(trim=True)
@@ -73,15 +96,20 @@ def stitch_groups_to_stitch_plan(stitch_groups, collapse_len=None, min_stitch_le
color_block.add_stitch(stop=True)
color_block = stitch_plan.new_color_block(color_block.color)
+ previous_stitch_group = stitch_group
+
+ if not need_tie_in:
+ # tie off at the end if we haven't already
+ lock_stitches = stitch_group.get_lock_stitches("end", disable_ties)
+ if lock_stitches:
+ color_block.add_stitches(stitches=lock_stitches)
+
if len(color_block) == 0:
# last block ended in a stop, so now we have an empty block
del stitch_plan.color_blocks[-1]
stitch_plan.filter_duplicate_stitches(min_stitch_len)
- if not disable_ties:
- stitch_plan.add_ties()
-
return stitch_plan
@@ -111,10 +139,6 @@ class StitchPlan(object):
for color_block in self:
color_block.filter_duplicate_stitches(min_stitch_len)
- def add_ties(self):
- # see ties.py
- add_ties(self)
-
def __iter__(self):
return iter(self.color_blocks)
diff --git a/lib/stitch_plan/ties.py b/lib/stitch_plan/ties.py
deleted file mode 100644
index a95f9805..00000000
--- a/lib/stitch_plan/ties.py
+++ /dev/null
@@ -1,73 +0,0 @@
-# Authors: see git history
-#
-# Copyright (c) 2010 Authors
-# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
-
-from copy import deepcopy
-
-from .stitch import Stitch
-from ..svg import PIXELS_PER_MM
-
-
-def add_tie(stitches, tie_path):
- if len(tie_path) < 2 or tie_path[0].no_ties:
- # It's from a manual stitch block, so don't add tie stitches. The user
- # will add them if they want them.
- return
-
- to_previous = tie_path[1] - tie_path[0]
- length = to_previous.length()
- if length > 0.5 * PIXELS_PER_MM:
- # Travel back one stitch, stopping halfway there.
- # Then go forward one stitch, stopping halfway between
- # again.
-
- # but travel at most 1.5mm
- length = min(length, 1.5 * PIXELS_PER_MM)
-
- direction = to_previous.unit()
- for delta in (0.5, 1.0, 0.5, 0):
- stitches.append(Stitch(tie_path[0] + delta * length * direction))
- else:
- # Too short to travel part of the way to the previous stitch; ust go
- # back and forth to it a couple times.
- for i in (1, 0, 1, 0):
- stitches.append(deepcopy(tie_path[i]))
-
-
-def add_tie_off(stitches):
- # tie_modus: 0 = both | 1 = before | 2 = after | 3 = neither
- if stitches[-1].tie_modus not in [1, 3] or stitches[-1].force_lock_stitches:
- add_tie(stitches, stitches[-1:-3:-1])
-
-
-def add_tie_in(stitches, upcoming_stitches):
- if stitches[0].tie_modus not in [2, 3]:
- add_tie(stitches, upcoming_stitches)
-
-
-def add_ties(stitch_plan):
- """Add tie-off before and after trims, jumps, and color changes."""
-
- need_tie_in = True
- for color_block in stitch_plan:
- new_stitches = []
- for i, stitch in enumerate(color_block.stitches):
- is_special = stitch.trim or stitch.jump or stitch.color_change or stitch.stop
-
- if is_special and not need_tie_in:
- add_tie_off(new_stitches)
- new_stitches.append(stitch)
- need_tie_in = True
- elif need_tie_in and not is_special:
- new_stitches.append(stitch)
- add_tie_in(new_stitches, upcoming_stitches=color_block.stitches[i:])
- need_tie_in = False
- else:
- new_stitches.append(stitch)
-
- color_block.replace_stitches(new_stitches)
-
- if not need_tie_in:
- # tie off at the end if we haven't already
- add_tie_off(color_block.stitches)
diff --git a/lib/stitches/fill.py b/lib/stitches/fill.py
index 3bd7761d..2c5cdffc 100644
--- a/lib/stitches/fill.py
+++ b/lib/stitches/fill.py
@@ -86,7 +86,7 @@ def stitch_row(stitches, beg, end, angle, row_spacing, max_stitch_length, stagge
offset = (first_stitch - beg).length()
while offset < segment_length:
- stitches.append(Stitch(beg + offset * row_direction, tags=('fill_row')))
+ stitches.append(Stitch(beg + offset * row_direction, tags=('fill_row',)))
offset += max_stitch_length
if (end - stitches[-1]).length() > 0.1 * PIXELS_PER_MM and not skip_last:
diff --git a/lib/svg/tags.py b/lib/svg/tags.py
index 5a7d31c2..60250dec 100644
--- a/lib/svg/tags.py
+++ b/lib/svg/tags.py
@@ -3,9 +3,8 @@
# Copyright (c) 2010 Authors
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
-from lxml import etree
-
import inkex
+from lxml import etree
etree.register_namespace("inkstitch", "http://inkstitch.org/namespace")
inkex.NSS['inkstitch'] = 'http://inkstitch.org/namespace'
@@ -53,6 +52,14 @@ INKSTITCH_ATTRIBS = {}
inkstitch_attribs = [
'ties',
'force_lock_stitches',
+ 'lock_start',
+ 'lock_start_scale_mm',
+ 'lock_start_scale_percent',
+ 'lock_custom_start',
+ 'lock_end',
+ 'lock_end_scale_mm',
+ 'lock_end_scale_percent',
+ 'lock_custom_end',
# clone
'clone',
'flip_angle',
diff --git a/lib/utils/string.py b/lib/utils/string.py
index e9204076..7ed99b77 100644
--- a/lib/utils/string.py
+++ b/lib/utils/string.py
@@ -7,7 +7,15 @@ def string_to_floats(string, delimiter=","):
"""Convert a string of delimiter-separated floats into a list of floats."""
floats = string.split(delimiter)
- return [float(num) for num in floats]
+ return [float(num) for num in floats if _is_float(num)]
+
+
+def _is_float(float_string):
+ try:
+ float(float_string)
+ return True
+ except ValueError:
+ return False
def remove_suffix(string, suffix):