summaryrefslogtreecommitdiff
path: root/lib/stitch_plan
diff options
context:
space:
mode:
Diffstat (limited to 'lib/stitch_plan')
-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
5 files changed, 292 insertions, 112 deletions
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)