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 | |
| parent | ed4aa55a733986436853e2ee7ad22f757b09fcb1 (diff) | |
Various lock stitch options (#2006)
Co-authored-by: Lex Neva
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/elements/element.py | 159 | ||||
| -rw-r--r-- | lib/elements/fill_stitch.py | 14 | ||||
| -rw-r--r-- | lib/elements/polyline.py | 6 | ||||
| -rw-r--r-- | lib/elements/satin_column.py | 12 | ||||
| -rw-r--r-- | lib/elements/stroke.py | 13 | ||||
| -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 | ||||
| -rw-r--r-- | lib/stitches/fill.py | 2 | ||||
| -rw-r--r-- | lib/svg/tags.py | 11 | ||||
| -rw-r--r-- | lib/utils/string.py | 10 |
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): |
