diff options
| author | Kaalleen <36401965+kaalleen@users.noreply.github.com> | 2023-02-27 16:05:52 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-02-27 16:05:52 +0100 |
| commit | ec076315bb8b5f901670fee1c06db028242b21fd (patch) | |
| tree | 17cd3413f48cad338d95452d2d0aac96aac44d01 /lib/stitch_plan | |
| parent | ed4aa55a733986436853e2ee7ad22f757b09fcb1 (diff) | |
Various lock stitch options (#2006)
Co-authored-by: Lex Neva
Diffstat (limited to 'lib/stitch_plan')
| -rw-r--r-- | lib/stitch_plan/lock_stitch.py | 224 | ||||
| -rw-r--r-- | lib/stitch_plan/stitch.py | 30 | ||||
| -rw-r--r-- | lib/stitch_plan/stitch_group.py | 21 | ||||
| -rw-r--r-- | lib/stitch_plan/stitch_plan.py | 56 | ||||
| -rw-r--r-- | lib/stitch_plan/ties.py | 73 |
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) |
