summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--lib/api/stitch_plan.py6
-rw-r--r--lib/commands.py1
-rw-r--r--lib/elements/auto_fill.py91
-rw-r--r--lib/elements/clone.py4
-rw-r--r--lib/elements/element.py36
-rw-r--r--lib/elements/empty_d_object.py2
-rw-r--r--lib/elements/fill.py9
-rw-r--r--lib/elements/image.py2
-rw-r--r--lib/elements/pattern.py33
-rw-r--r--lib/elements/polyline.py9
-rw-r--r--lib/elements/satin_column.py72
-rw-r--r--lib/elements/stroke.py9
-rw-r--r--lib/elements/text.py2
-rw-r--r--lib/elements/utils.py5
-rw-r--r--lib/extensions/__init__.py2
-rw-r--r--lib/extensions/base.py8
-rw-r--r--lib/extensions/lettering.py4
-rw-r--r--lib/extensions/output.py6
-rw-r--r--lib/extensions/params.py4
-rw-r--r--lib/extensions/print_pdf.py6
-rw-r--r--lib/extensions/selection_to_pattern.py63
-rw-r--r--lib/extensions/stitch_plan_preview.py6
-rw-r--r--lib/extensions/zip.py6
-rw-r--r--lib/gui/simulator.py6
-rw-r--r--lib/patterns.py106
-rw-r--r--lib/stitch_plan/__init__.py4
-rw-r--r--lib/stitch_plan/color_block.py143
-rw-r--r--lib/stitch_plan/stitch.py54
-rw-r--r--lib/stitch_plan/stitch_group.py64
-rw-r--r--lib/stitch_plan/stitch_plan.py176
-rw-r--r--lib/stitches/auto_fill.py8
-rw-r--r--lib/stitches/fill.py12
-rw-r--r--lib/utils/geometry.py22
-rw-r--r--templates/selection_to_pattern.xml17
34 files changed, 689 insertions, 309 deletions
diff --git a/lib/api/stitch_plan.py b/lib/api/stitch_plan.py
index 2011b592..6d64d781 100644
--- a/lib/api/stitch_plan.py
+++ b/lib/api/stitch_plan.py
@@ -5,7 +5,7 @@
from flask import Blueprint, g, jsonify
-from ..stitch_plan import patches_to_stitch_plan
+from ..stitch_plan import stitch_groups_to_stitch_plan
stitch_plan = Blueprint('stitch_plan', __name__)
@@ -18,7 +18,7 @@ def get_stitch_plan():
metadata = g.extension.get_inkstitch_metadata()
collapse_len = metadata['collapse_len_mm']
- patches = g.extension.elements_to_patches(g.extension.elements)
- stitch_plan = patches_to_stitch_plan(patches, collapse_len=collapse_len)
+ patches = g.extension.elements_to_stitch_groups(g.extension.elements)
+ stitch_plan = stitch_groups_to_stitch_plan(patches, collapse_len=collapse_len)
return jsonify(stitch_plan)
diff --git a/lib/commands.py b/lib/commands.py
index 8fccc63f..c9da782a 100644
--- a/lib/commands.py
+++ b/lib/commands.py
@@ -44,7 +44,6 @@ COMMANDS = {
# L10N command attached to an object
"satin_cut_point": N_("Satin cut point (use with Cut Satin Column)"),
-
# L10N command that affects a layer
"ignore_layer": N_("Ignore layer (do not stitch any objects in this layer)"),
diff --git a/lib/elements/auto_fill.py b/lib/elements/auto_fill.py
index cf7a44a7..fbbd86d3 100644
--- a/lib/elements/auto_fill.py
+++ b/lib/elements/auto_fill.py
@@ -9,13 +9,14 @@ import traceback
from shapely import geometry as shgeo
+from .element import param
+from .fill import Fill
+from .validation import ValidationWarning
from ..i18n import _
+from ..stitch_plan import StitchGroup
from ..stitches import auto_fill
from ..svg.tags import INKSCAPE_LABEL
from ..utils import cache, version
-from .element import Patch, param
-from .fill import Fill
-from .validation import ValidationWarning
class SmallShapeWarning(ValidationWarning):
@@ -212,8 +213,8 @@ class AutoFill(Fill):
else:
return None
- def to_patches(self, last_patch):
- stitches = []
+ def to_stitch_groups(self, last_patch):
+ stitch_groups = []
starting_point = self.get_starting_point(last_patch)
ending_point = self.get_ending_point()
@@ -221,29 +222,40 @@ class AutoFill(Fill):
try:
if self.fill_underlay:
for i in range(len(self.fill_underlay_angle)):
- stitches.extend(auto_fill(self.underlay_shape,
- self.fill_underlay_angle[i],
- self.fill_underlay_row_spacing,
- self.fill_underlay_row_spacing,
- self.fill_underlay_max_stitch_length,
- self.running_stitch_length,
- self.staggers,
- self.fill_underlay_skip_last,
- starting_point,
- underpath=self.underlay_underpath))
- starting_point = stitches[-1]
-
- stitches.extend(auto_fill(self.fill_shape,
- self.angle,
- self.row_spacing,
- self.end_row_spacing,
- self.max_stitch_length,
- self.running_stitch_length,
- self.staggers,
- self.skip_last,
- starting_point,
- ending_point,
- self.underpath))
+ underlay = StitchGroup(
+ color=self.color,
+ tags=("auto_fill", "auto_fill_underlay"),
+ stitches=auto_fill(
+ self.underlay_shape,
+ self.fill_underlay_angle[i],
+ self.fill_underlay_row_spacing,
+ self.fill_underlay_row_spacing,
+ self.fill_underlay_max_stitch_length,
+ self.running_stitch_length,
+ self.staggers,
+ self.fill_underlay_skip_last,
+ starting_point,
+ underpath=self.underlay_underpath))
+ stitch_groups.append(underlay)
+
+ starting_point = underlay.stitches[-1]
+
+ stitch_group = StitchGroup(
+ color=self.color,
+ tags=("auto_fill", "auto_fill_top"),
+ stitches=auto_fill(
+ self.fill_shape,
+ self.angle,
+ self.row_spacing,
+ self.end_row_spacing,
+ self.max_stitch_length,
+ self.running_stitch_length,
+ self.staggers,
+ self.skip_last,
+ starting_point,
+ ending_point,
+ self.underpath))
+ stitch_groups.append(stitch_group)
except Exception:
if hasattr(sys, 'gettrace') and sys.gettrace():
# if we're debugging, let the exception bubble up
@@ -261,18 +273,19 @@ class AutoFill(Fill):
self.fatal(message)
- return [Patch(stitches=stitches, color=self.color)]
+ return stitch_groups
+
- def validation_warnings(self):
- if self.shape.area < 20:
- label = self.node.get(INKSCAPE_LABEL) or self.node.get("id")
- yield SmallShapeWarning(self.shape.centroid, label)
+def validation_warnings(self):
+ if self.shape.area < 20:
+ label = self.node.get(INKSCAPE_LABEL) or self.node.get("id")
+ yield SmallShapeWarning(self.shape.centroid, label)
- if self.shrink_or_grow_shape(self.expand, True).is_empty:
- yield ExpandWarning(self.shape.centroid)
+ if self.shrink_or_grow_shape(self.expand, True).is_empty:
+ yield ExpandWarning(self.shape.centroid)
- if self.shrink_or_grow_shape(-self.fill_underlay_inset, True).is_empty:
- yield UnderlayInsetWarning(self.shape.centroid)
+ if self.shrink_or_grow_shape(-self.fill_underlay_inset, True).is_empty:
+ yield UnderlayInsetWarning(self.shape.centroid)
- for warning in super(AutoFill, self).validation_warnings():
- yield warning
+ for warning in super(AutoFill, self).validation_warnings():
+ yield warning
diff --git a/lib/elements/clone.py b/lib/elements/clone.py
index 6dafa63d..a9e10d94 100644
--- a/lib/elements/clone.py
+++ b/lib/elements/clone.py
@@ -93,7 +93,7 @@ class Clone(EmbroideryElement):
return elements
- def to_patches(self, last_patch=None):
+ def to_stitch_groups(self, last_patch=None):
patches = []
source_node = get_clone_source(self.node)
@@ -123,7 +123,7 @@ class Clone(EmbroideryElement):
elements = self.clone_to_element(self.node)
for element in elements:
- patches.extend(element.to_patches(last_patch))
+ patches.extend(element.to_stitch_groups(last_patch))
return patches
diff --git a/lib/elements/element.py b/lib/elements/element.py
index 0b001f0b..f06982b2 100644
--- a/lib/elements/element.py
+++ b/lib/elements/element.py
@@ -11,40 +11,13 @@ from inkex import bezier
from ..commands import find_commands
from ..i18n import _
+from ..patterns import apply_patterns
from ..svg import (PIXELS_PER_MM, apply_transforms, convert_length,
get_node_transform)
from ..svg.tags import INKSCAPE_LABEL, INKSTITCH_ATTRIBS
from ..utils import Point, cache
-class Patch:
- """A raw collection of stitches with attached instructions."""
-
- def __init__(self, color=None, stitches=None, trim_after=False, stop_after=False, tie_modus=0, stitch_as_is=False):
- self.color = color
- self.stitches = stitches or []
- self.trim_after = trim_after
- self.stop_after = stop_after
- self.tie_modus = tie_modus
- self.stitch_as_is = stitch_as_is
-
- def __add__(self, other):
- if isinstance(other, Patch):
- return Patch(self.color, self.stitches + other.stitches)
- else:
- raise TypeError("Patch can only be added to another Patch")
-
- def __len__(self):
- # This method allows `len(patch)` and `if patch:
- return len(self.stitches)
-
- def add_stitch(self, stitch):
- self.stitches.append(stitch)
-
- def reverse(self):
- return Patch(self.color, self.stitches[::-1])
-
-
class Param(object):
def __init__(self, name, description, unit=None, values=[], type=None, group=None, inverse=False,
options=[], default=None, tooltip=None, sort_index=0):
@@ -328,13 +301,14 @@ class EmbroideryElement(object):
def stop_after(self):
return self.get_boolean_param('stop_after', False)
- def to_patches(self, last_patch):
- raise NotImplementedError("%s must implement to_patches()" % self.__class__.__name__)
+ def to_stitch_groups(self, last_patch):
+ raise NotImplementedError("%s must implement to_stitch_groups()" % self.__class__.__name__)
def embroider(self, last_patch):
self.validate()
- patches = self.to_patches(last_patch)
+ patches = self.to_stitch_groups(last_patch)
+ apply_patterns(patches, self.node)
for patch in patches:
patch.tie_modus = self.ties
diff --git a/lib/elements/empty_d_object.py b/lib/elements/empty_d_object.py
index 19fb58a4..3c24f333 100644
--- a/lib/elements/empty_d_object.py
+++ b/lib/elements/empty_d_object.py
@@ -23,5 +23,5 @@ class EmptyDObject(EmbroideryElement):
label = self.node.get(INKSCAPE_LABEL) or self.node.get("id")
yield EmptyD((0, 0), label)
- def to_patches(self, last_patch):
+ def to_stitch_groups(self, last_patch):
return []
diff --git a/lib/elements/fill.py b/lib/elements/fill.py
index b6799165..442922b6 100644
--- a/lib/elements/fill.py
+++ b/lib/elements/fill.py
@@ -10,12 +10,13 @@ import re
from shapely import geometry as shgeo
from shapely.validation import explain_validity
+from .element import EmbroideryElement, param
+from .validation import ValidationError
from ..i18n import _
+from ..stitch_plan import StitchGroup
from ..stitches import legacy_fill
from ..svg import PIXELS_PER_MM
from ..utils import cache
-from .element import EmbroideryElement, Patch, param
-from .validation import ValidationError
class UnconnectedError(ValidationError):
@@ -189,7 +190,7 @@ class Fill(EmbroideryElement):
else:
yield InvalidShapeError((x, y))
- def to_patches(self, last_patch):
+ def to_stitch_groups(self, last_patch):
stitch_lists = legacy_fill(self.shape,
self.angle,
self.row_spacing,
@@ -198,4 +199,4 @@ class Fill(EmbroideryElement):
self.flip,
self.staggers,
self.skip_last)
- return [Patch(stitches=stitch_list, color=self.color) for stitch_list in stitch_lists]
+ return [StitchGroup(stitches=stitch_list, color=self.color) for stitch_list in stitch_lists]
diff --git a/lib/elements/image.py b/lib/elements/image.py
index 0828b5ef..73a46871 100644
--- a/lib/elements/image.py
+++ b/lib/elements/image.py
@@ -29,5 +29,5 @@ class ImageObject(EmbroideryElement):
def validation_warnings(self):
yield ImageTypeWarning(self.center())
- def to_patches(self, last_patch):
+ def to_stitch_groups(self, last_patch):
return []
diff --git a/lib/elements/pattern.py b/lib/elements/pattern.py
new file mode 100644
index 00000000..4b92d366
--- /dev/null
+++ b/lib/elements/pattern.py
@@ -0,0 +1,33 @@
+# Authors: see git history
+#
+# Copyright (c) 2010 Authors
+# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
+
+import inkex
+
+from ..i18n import _
+from .element import EmbroideryElement
+from .validation import ObjectTypeWarning
+
+
+class PatternWarning(ObjectTypeWarning):
+ name = _("Pattern Element")
+ description = _("This element will not be embroidered. "
+ "It will appear as a pattern applied to objects in the same group as it. "
+ "Objects in sub-groups will be ignored.")
+ steps_to_solve = [
+ _("To disable pattern mode, remove the pattern marker:"),
+ _('* Open the Fill and Stroke panel (Objects > Fill and Stroke)'),
+ _('* Go to the Stroke style tab'),
+ _('* Under "Markers" choose the first (empty) option in the first dropdown list.')
+ ]
+
+
+class PatternObject(EmbroideryElement):
+
+ def validation_warnings(self):
+ repr_point = next(inkex.Path(self.parse_path()).end_points)
+ yield PatternWarning(repr_point)
+
+ def to_stitch_groups(self, last_patch):
+ return []
diff --git a/lib/elements/polyline.py b/lib/elements/polyline.py
index ead2c322..e923aac0 100644
--- a/lib/elements/polyline.py
+++ b/lib/elements/polyline.py
@@ -6,11 +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, Patch, param
-from .validation import ValidationWarning
class PolylineWarning(ValidationWarning):
@@ -100,8 +101,8 @@ class Polyline(EmbroideryElement):
def validation_warnings(self):
yield PolylineWarning(self.points[0])
- def to_patches(self, last_patch):
- patch = Patch(color=self.color)
+ def to_stitch_groups(self, last_patch):
+ patch = StitchGroup(color=self.color)
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 d72680b7..cf31c2af 100644
--- a/lib/elements/satin_column.py
+++ b/lib/elements/satin_column.py
@@ -11,11 +11,12 @@ from shapely import affinity as shaffinity
from shapely import geometry as shgeo
from shapely.ops import nearest_points
+from .element import EmbroideryElement, param
+from .validation import ValidationError, ValidationWarning
from ..i18n import _
+from ..stitch_plan import StitchGroup
from ..svg import line_strings_to_csp, point_lists_to_csp
from ..utils import Point, cache, collapse_duplicate_point, cut
-from .element import EmbroideryElement, Patch, param
-from .validation import ValidationError, ValidationWarning
class SatinHasFillError(ValidationError):
@@ -81,6 +82,14 @@ class SatinColumn(EmbroideryElement):
return self.get_boolean_param("e_stitch")
@property
+ @param('max_stitch_length_mm',
+ _('Maximum stitch length'),
+ tooltip=_('Maximum stitch length for split stitches.'),
+ type='float', unit="mm")
+ def max_stitch_length(self):
+ return self.get_float_param("max_stitch_length_mm") or None
+
+ @property
def color(self):
return self.get_style("stroke")
@@ -708,7 +717,10 @@ class SatinColumn(EmbroideryElement):
# other.
forward, back = self.plot_points_on_rails(self.contour_underlay_stitch_length,
-self.contour_underlay_inset)
- return Patch(color=self.color, stitches=(forward + list(reversed(back))))
+ return StitchGroup(
+ color=self.color,
+ tags=("satin_column", "satin_column_underlay", "satin_contour_underlay"),
+ stitches=(forward + list(reversed(back))))
def do_center_walk(self):
# Center walk underlay is just a running stitch down and back on the
@@ -717,7 +729,10 @@ class SatinColumn(EmbroideryElement):
# Do it like contour underlay, but inset all the way to the center.
forward, back = self.plot_points_on_rails(self.center_walk_underlay_stitch_length,
-100000)
- return Patch(color=self.color, stitches=(forward + list(reversed(back))))
+ return StitchGroup(
+ color=self.color,
+ tags=("satin_column", "satin_column_underlay", "satin_center_walk"),
+ stitches=(forward + list(reversed(back))))
def do_zigzag_underlay(self):
# zigzag underlay, usually done at a much lower density than the
@@ -730,7 +745,7 @@ class SatinColumn(EmbroideryElement):
# "German underlay" described here:
# http://www.mrxstitch.com/underlay-what-lies-beneath-machine-embroidery/
- patch = Patch(color=self.color)
+ patch = StitchGroup(color=self.color)
sides = self.plot_points_on_rails(self.zigzag_underlay_spacing / 2.0,
-self.zigzag_underlay_inset)
@@ -745,6 +760,7 @@ class SatinColumn(EmbroideryElement):
for point in chain.from_iterable(zip(*sides)):
patch.add_stitch(point)
+ patch.add_tags(("satin_column", "satin_column_underlay", "satin_zigzag_underlay"))
return patch
def do_satin(self):
@@ -756,7 +772,10 @@ class SatinColumn(EmbroideryElement):
# print >> dbg, "satin", self.zigzag_spacing, self.pull_compensation
- patch = Patch(color=self.color)
+ if self.max_stitch_length:
+ return self.do_split_stitch()
+
+ patch = StitchGroup(color=self.color)
sides = self.plot_points_on_rails(self.zigzag_spacing, self.pull_compensation)
@@ -764,6 +783,7 @@ class SatinColumn(EmbroideryElement):
for point in chain.from_iterable(zip(*sides)):
patch.add_stitch(point)
+ patch.add_tags(("satin_column", "satin_column_edge"))
return patch
def do_e_stitch(self):
@@ -774,7 +794,7 @@ class SatinColumn(EmbroideryElement):
# print >> dbg, "satin", self.zigzag_spacing, self.pull_compensation
- patch = Patch(color=self.color)
+ patch = StitchGroup(color=self.color)
sides = self.plot_points_on_rails(self.zigzag_spacing, self.pull_compensation)
@@ -785,16 +805,50 @@ class SatinColumn(EmbroideryElement):
patch.add_stitch(right)
patch.add_stitch(left)
+ patch.add_tags(("satin_column", "e_stitch"))
+ return patch
+
+ def do_split_stitch(self):
+ # stitches exceeding the maximum stitch length will be divided into equal parts through additional stitches
+ patch = StitchGroup(color=self.color)
+ sides = self.plot_points_on_rails(self.zigzag_spacing, self.pull_compensation)
+ for i, (left, right) in enumerate(zip(*sides)):
+ patch.add_stitch(left)
+ patch.stitches[-1].add_tags(("satin_column", "satin_column_edge"))
+ points, count = self._get_split_points(left, right)
+ for point in points:
+ patch.add_stitch(point)
+ patch.stitches[-1].add_tags(("satin_column", "satin_split_stitch"))
+ patch.add_stitch(right)
+ patch.stitches[-1].add_tags(("satin_column", "satin_column_edge"))
+ # it is possible that the way back has a different length from the first
+ # but it looks ugly if the points differ too much
+ # so let's make sure they have at least the same amount of divisions
+ if not i+1 >= len(sides[0]):
+ points, count = self._get_split_points(right, sides[0][i+1], count)
+ for point in points:
+ patch.add_stitch(point)
+ patch.stitches[-1].add_tags(("satin_column", "satin_split_stitch"))
return patch
- def to_patches(self, last_patch):
+ def _get_split_points(self, left, right, count=None):
+ points = []
+ distance = left.distance(right)
+ split_count = count or int(-(-distance // self.max_stitch_length))
+ for i in range(split_count):
+ line = shgeo.LineString((left, right))
+ split_point = line.interpolate((i+1)/split_count, normalized=True)
+ points.append(Point(split_point.x, split_point.y))
+ return [points, split_count]
+
+ def to_stitch_groups(self, last_patch):
# Stitch a variable-width satin column, zig-zagging between two paths.
# The algorithm will draw zigzags between each consecutive pair of
# beziers. The boundary points between beziers serve as "checkpoints",
# allowing the user to control how the zigzags flow around corners.
- patch = Patch(color=self.color)
+ patch = StitchGroup(color=self.color)
if self.center_walk_underlay:
patch += self.do_center_walk()
diff --git a/lib/elements/stroke.py b/lib/elements/stroke.py
index 39a8f6e3..763167ad 100644
--- a/lib/elements/stroke.py
+++ b/lib/elements/stroke.py
@@ -7,11 +7,12 @@ import sys
import shapely.geometry
+from .element import EmbroideryElement, param
from ..i18n import _
+from ..stitch_plan import StitchGroup
from ..stitches import bean_stitch, running_stitch
from ..svg import parse_length_with_units
from ..utils import Point, cache
-from .element import EmbroideryElement, Patch, param
warned_about_legacy_running_stitch = False
@@ -190,15 +191,15 @@ class Stroke(EmbroideryElement):
stitches = running_stitch(repeated_path, stitch_length)
- return Patch(self.color, stitches)
+ return StitchGroup(self.color, stitches)
- def to_patches(self, last_patch):
+ def to_stitch_groups(self, last_patch):
patches = []
for path in self.paths:
path = [Point(x, y) for x, y in path]
if self.manual_stitch_mode:
- patch = Patch(color=self.color, stitches=path, stitch_as_is=True)
+ patch = StitchGroup(color=self.color, stitches=path, stitch_as_is=True)
elif self.is_running_stitch():
patch = self.running_stitch(path, self.running_stitch_length)
diff --git a/lib/elements/text.py b/lib/elements/text.py
index dbf76c85..8a3846c0 100644
--- a/lib/elements/text.py
+++ b/lib/elements/text.py
@@ -29,5 +29,5 @@ class TextObject(EmbroideryElement):
def validation_warnings(self):
yield TextTypeWarning(self.pointer())
- def to_patches(self, last_patch):
+ def to_stitch_groups(self, last_patch):
return []
diff --git a/lib/elements/utils.py b/lib/elements/utils.py
index aceab485..99df7002 100644
--- a/lib/elements/utils.py
+++ b/lib/elements/utils.py
@@ -4,6 +4,7 @@
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
from ..commands import is_command
+from ..patterns import is_pattern
from ..svg.tags import (EMBROIDERABLE_TAGS, SVG_IMAGE_TAG, SVG_PATH_TAG,
SVG_POLYLINE_TAG, SVG_TEXT_TAG)
from .auto_fill import AutoFill
@@ -12,6 +13,7 @@ from .element import EmbroideryElement
from .empty_d_object import EmptyDObject
from .fill import Fill
from .image import ImageObject
+from .pattern import PatternObject
from .polyline import Polyline
from .satin_column import SatinColumn
from .stroke import Stroke
@@ -28,6 +30,9 @@ def node_to_elements(node): # noqa: C901
elif node.tag == SVG_PATH_TAG and not node.get('d', ''):
return [EmptyDObject(node)]
+ elif is_pattern(node):
+ return [PatternObject(node)]
+
elif node.tag in EMBROIDERABLE_TAGS:
element = EmbroideryElement(node)
diff --git a/lib/extensions/__init__.py b/lib/extensions/__init__.py
index 64e6af02..83a522f2 100644
--- a/lib/extensions/__init__.py
+++ b/lib/extensions/__init__.py
@@ -30,6 +30,7 @@ from .params import Params
from .print_pdf import Print
from .remove_embroidery_settings import RemoveEmbroiderySettings
from .reorder import Reorder
+from .selection_to_pattern import SelectionToPattern
from .simulator import Simulator
from .stitch_plan_preview import StitchPlanPreview
from .zip import Zip
@@ -42,6 +43,7 @@ __all__ = extensions = [StitchPlanPreview,
Output,
Zip,
Flip,
+ SelectionToPattern,
ObjectCommands,
LayerCommands,
GlobalCommands,
diff --git a/lib/extensions/base.py b/lib/extensions/base.py
index 057a0e63..828e3685 100644
--- a/lib/extensions/base.py
+++ b/lib/extensions/base.py
@@ -16,6 +16,7 @@ from ..commands import is_command, layer_commands
from ..elements import EmbroideryElement, nodes_to_elements
from ..elements.clone import is_clone
from ..i18n import _
+from ..patterns import is_pattern
from ..svg import generate_unique_id
from ..svg.tags import (CONNECTOR_TYPE, EMBROIDERABLE_TAGS, INKSCAPE_GROUPMODE,
NOT_EMBROIDERABLE_TAGS, SVG_DEFS_TAG, SVG_GROUP_TAG)
@@ -160,9 +161,10 @@ class InkstitchExtension(inkex.Effect):
if selected:
if node.tag == SVG_GROUP_TAG:
pass
- elif getattr(node, "get_path", None):
+ elif (node.tag in EMBROIDERABLE_TAGS or is_clone(node)) and not is_pattern(node):
nodes.append(node)
- elif troubleshoot and (node.tag in NOT_EMBROIDERABLE_TAGS or node.tag in EMBROIDERABLE_TAGS or is_clone(node)):
+ # add images, text and patterns for the troubleshoot extension
+ elif troubleshoot and (node.tag in NOT_EMBROIDERABLE_TAGS or is_pattern(node)):
nodes.append(node)
return nodes
@@ -186,7 +188,7 @@ class InkstitchExtension(inkex.Effect):
selected.append(node)
return selected
- def elements_to_patches(self, elements):
+ def elements_to_stitch_groups(self, elements):
patches = []
for element in elements:
if patches:
diff --git a/lib/extensions/lettering.py b/lib/extensions/lettering.py
index 875dea03..312a47ce 100644
--- a/lib/extensions/lettering.py
+++ b/lib/extensions/lettering.py
@@ -30,9 +30,11 @@ class LetteringFrame(wx.Frame):
def __init__(self, *args, **kwargs):
# This is necessary because of https://github.com/inkstitch/inkstitch/issues/1186
- if sys.platform.startswith('win'):
+ if sys.platform.startswith('win32'):
import locale
locale.setlocale(locale.LC_ALL, "C")
+ lc = wx.Locale()
+ lc.Init(wx.LANGUAGE_DEFAULT)
# begin wxGlade: MyFrame.__init__
self.group = kwargs.pop('group')
diff --git a/lib/extensions/output.py b/lib/extensions/output.py
index e621f1b6..7cc12ee0 100644
--- a/lib/extensions/output.py
+++ b/lib/extensions/output.py
@@ -8,7 +8,7 @@ import sys
import tempfile
from ..output import write_embroidery_file
-from ..stitch_plan import patches_to_stitch_plan
+from ..stitch_plan import stitch_groups_to_stitch_plan
from .base import InkstitchExtension
@@ -52,8 +52,8 @@ class Output(InkstitchExtension):
self.metadata = self.get_inkstitch_metadata()
collapse_len = self.metadata['collapse_len_mm']
- patches = self.elements_to_patches(self.elements)
- stitch_plan = patches_to_stitch_plan(patches, collapse_len=collapse_len, disable_ties=self.settings.get('laser_mode', False))
+ patches = self.elements_to_stitch_groups(self.elements)
+ stitch_plan = stitch_groups_to_stitch_plan(patches, collapse_len=collapse_len, disable_ties=self.settings.get('laser_mode', False))
temp_file = tempfile.NamedTemporaryFile(suffix=".%s" % self.file_extension, delete=False)
diff --git a/lib/extensions/params.py b/lib/extensions/params.py
index 82cc9be9..c96b9691 100644
--- a/lib/extensions/params.py
+++ b/lib/extensions/params.py
@@ -331,9 +331,11 @@ class ParamsTab(ScrolledPanel):
class SettingsFrame(wx.Frame):
def __init__(self, *args, **kwargs):
# This is necessary because of https://github.com/inkstitch/inkstitch/issues/1186
- if sys.platform.startswith('win'):
+ if sys.platform.startswith('win32'):
import locale
locale.setlocale(locale.LC_ALL, "C")
+ lc = wx.Locale()
+ lc.Init(wx.LANGUAGE_DEFAULT)
# begin wxGlade: MyFrame.__init__
self.tabs_factory = kwargs.pop('tabs_factory', [])
diff --git a/lib/extensions/print_pdf.py b/lib/extensions/print_pdf.py
index 0facdf92..e5cb25d8 100644
--- a/lib/extensions/print_pdf.py
+++ b/lib/extensions/print_pdf.py
@@ -23,7 +23,7 @@ from werkzeug.serving import make_server
from ..gui import open_url
from ..i18n import get_languages
from ..i18n import translation as inkstitch_translation
-from ..stitch_plan import patches_to_stitch_plan
+from ..stitch_plan import stitch_groups_to_stitch_plan
from ..svg import render_stitch_plan
from ..svg.tags import INKSCAPE_GROUPMODE
from ..threads import ThreadCatalog
@@ -302,8 +302,8 @@ class Print(InkstitchExtension):
self.metadata = self.get_inkstitch_metadata()
collapse_len = self.metadata['collapse_len_mm']
- patches = self.elements_to_patches(self.elements)
- stitch_plan = patches_to_stitch_plan(patches, collapse_len=collapse_len)
+ patches = self.elements_to_stitch_groups(self.elements)
+ stitch_plan = stitch_groups_to_stitch_plan(patches, collapse_len=collapse_len)
palette = ThreadCatalog().match_and_apply_palette(stitch_plan, self.get_inkstitch_metadata()['thread-palette'])
overview_svg, color_block_svgs = self.render_svgs(stitch_plan, realistic=False)
diff --git a/lib/extensions/selection_to_pattern.py b/lib/extensions/selection_to_pattern.py
new file mode 100644
index 00000000..41f89a83
--- /dev/null
+++ b/lib/extensions/selection_to_pattern.py
@@ -0,0 +1,63 @@
+# Authors: see git history
+#
+# Copyright (c) 2021 Authors
+# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
+
+import inkex
+from lxml import etree
+
+from ..i18n import _
+from ..svg.tags import EMBROIDERABLE_TAGS, SVG_DEFS_TAG
+from .base import InkstitchExtension
+
+
+class SelectionToPattern(InkstitchExtension):
+
+ def effect(self):
+ if not self.get_elements():
+ return
+
+ if not self.svg.selected:
+ inkex.errormsg(_("Please select at least one object to be marked as a pattern."))
+ return
+
+ for pattern in self.get_nodes():
+ if pattern.tag in EMBROIDERABLE_TAGS:
+ self.set_marker(pattern)
+
+ def set_marker(self, node):
+ xpath = ".//marker[@id='inkstitch-pattern-marker']"
+ pattern_marker = self.document.xpath(xpath)
+
+ if not pattern_marker:
+ # get or create def element
+ defs = self.document.find(SVG_DEFS_TAG)
+ if defs is None:
+ defs = etree.SubElement(self.document, SVG_DEFS_TAG)
+
+ # insert marker
+ marker = """<marker
+ refX="10"
+ refY="5"
+ orient="auto"
+ id="inkstitch-pattern-marker">
+ <g
+ id="inkstitch-pattern-group">
+ <path
+ style="fill:#fafafa;stroke:#ff5500;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:1, 1;stroke-dashoffset:0;stroke-opacity:1;fill-opacity:0.8;"
+ d="M 10.12911,5.2916678 A 4.8374424,4.8374426 0 0 1 5.2916656,10.12911 4.8374424,4.8374426 0 0 1 0.45422399,5.2916678 4.8374424,4.8374426 0 0 1 5.2916656,0.45422399 4.8374424,4.8374426 0 0 1 10.12911,5.2916678 Z"
+ id="inkstitch-pattern-marker-circle" />
+ <path
+ style="fill:none;stroke:#000000;stroke-width:0.4;stroke-linecap:round;stroke-miterlimit:4;"
+ id="inkstitch-pattern-marker-spiral"
+ d="M 4.9673651,5.7245662 C 4.7549848,5.7646159 4.6247356,5.522384 4.6430021,5.3419847 4.6765851,5.0103151 5.036231,4.835347 5.3381858,4.8987426 5.7863901,4.9928495 6.0126802,5.4853625 5.9002872,5.9065088 5.7495249,6.4714237 5.1195537,6.7504036 4.5799191,6.5874894 3.898118,6.3816539 3.5659013,5.6122905 3.7800789,4.9545192 4.0402258,4.1556558 4.9498996,3.7699484 5.7256318,4.035839 6.6416744,4.3498087 7.0810483,5.4003986 6.7631909,6.2939744 6.395633,7.3272552 5.2038143,7.8204128 4.1924535,7.4503931 3.0418762,7.0294421 2.4948761,5.6961604 2.9171752,4.567073 3.3914021,3.2991406 4.8663228,2.6982592 6.1130974,3.1729158 7.4983851,3.7003207 8.1531869,5.3169977 7.6260947,6.6814205 7.0456093,8.1841025 5.2870784,8.8928844 3.8050073,8.3132966 2.1849115,7.6797506 1.4221671,5.7793073 2.0542715,4.1796074 2.7408201,2.4420977 4.7832541,1.6253548 6.5005435,2.310012 8.3554869,3.0495434 9.2262638,5.2339874 8.4890181,7.0688861 8.4256397,7.2266036 8.3515789,7.379984 8.2675333,7.5277183" />
+ </g>
+ </marker>""" # noqa: E501
+ defs.append(etree.fromstring(marker))
+
+ # attach marker to node
+ style = node.get('style') or ''
+ style = style.split(";")
+ style = [i for i in style if not i.startswith('marker-start')]
+ style.append('marker-start:url(#inkstitch-pattern-marker)')
+ node.set('style', ";".join(style))
diff --git a/lib/extensions/stitch_plan_preview.py b/lib/extensions/stitch_plan_preview.py
index 40ad6a2a..c50fa738 100644
--- a/lib/extensions/stitch_plan_preview.py
+++ b/lib/extensions/stitch_plan_preview.py
@@ -3,7 +3,7 @@
# Copyright (c) 2010 Authors
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
-from ..stitch_plan import patches_to_stitch_plan
+from ..stitch_plan import stitch_groups_to_stitch_plan
from ..svg import render_stitch_plan
from .base import InkstitchExtension
@@ -23,8 +23,8 @@ class StitchPlanPreview(InkstitchExtension):
realistic = False
self.metadata = self.get_inkstitch_metadata()
collapse_len = self.metadata['collapse_len_mm']
- patches = self.elements_to_patches(self.elements)
- stitch_plan = patches_to_stitch_plan(patches, collapse_len=collapse_len)
+ patches = self.elements_to_stitch_groups(self.elements)
+ stitch_plan = stitch_groups_to_stitch_plan(patches, collapse_len=collapse_len)
render_stitch_plan(svg, stitch_plan, realistic)
# translate stitch plan to the right side of the canvas
diff --git a/lib/extensions/zip.py b/lib/extensions/zip.py
index 605b4573..22654560 100644
--- a/lib/extensions/zip.py
+++ b/lib/extensions/zip.py
@@ -16,7 +16,7 @@ import pyembroidery
from ..i18n import _
from ..output import write_embroidery_file
-from ..stitch_plan import patches_to_stitch_plan
+from ..stitch_plan import stitch_groups_to_stitch_plan
from ..threads import ThreadCatalog
from .base import InkstitchExtension
@@ -43,8 +43,8 @@ class Zip(InkstitchExtension):
self.metadata = self.get_inkstitch_metadata()
collapse_len = self.metadata['collapse_len_mm']
- patches = self.elements_to_patches(self.elements)
- stitch_plan = patches_to_stitch_plan(patches, collapse_len=collapse_len)
+ patches = self.elements_to_stitch_groups(self.elements)
+ stitch_plan = stitch_groups_to_stitch_plan(patches, collapse_len=collapse_len)
base_file_name = self.get_base_file_name()
path = tempfile.mkdtemp()
diff --git a/lib/gui/simulator.py b/lib/gui/simulator.py
index 3e1f68c5..d031590b 100644
--- a/lib/gui/simulator.py
+++ b/lib/gui/simulator.py
@@ -11,7 +11,7 @@ import wx
from wx.lib.intctrl import IntCtrl
from ..i18n import _
-from ..stitch_plan import patches_to_stitch_plan, stitch_plan_from_file
+from ..stitch_plan import stitch_groups_to_stitch_plan, stitch_plan_from_file
from ..svg import PIXELS_PER_MM
# L10N command label at bottom of simulator window
@@ -733,7 +733,7 @@ class SimulatorPreview(Thread):
The parent is expected to be a wx.Window and also implement the following methods:
def generate_patches(self, abort_event):
- Produce an list of Patch instances. This method will be
+ Produce an list of StitchGroup instances. This method will be
invoked in a background thread and it is expected that it may
take awhile.
@@ -789,7 +789,7 @@ class SimulatorPreview(Thread):
return
if patches and not self.refresh_needed.is_set():
- stitch_plan = patches_to_stitch_plan(patches)
+ stitch_plan = stitch_groups_to_stitch_plan(patches)
# GUI stuff needs to happen in the main thread, so we ask the main
# thread to call refresh_simulator().
diff --git a/lib/patterns.py b/lib/patterns.py
new file mode 100644
index 00000000..8a0c8449
--- /dev/null
+++ b/lib/patterns.py
@@ -0,0 +1,106 @@
+# Authors: see git history
+#
+# Copyright (c) 2010 Authors
+# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
+
+import inkex
+from shapely import geometry as shgeo
+
+from .stitch_plan import Stitch
+from .svg.tags import EMBROIDERABLE_TAGS
+from .utils import Point
+
+
+def is_pattern(node):
+ if node.tag not in EMBROIDERABLE_TAGS:
+ return False
+ style = node.get('style') or ''
+ return "marker-start:url(#inkstitch-pattern-marker)" in style
+
+
+def apply_patterns(patches, node):
+ patterns = _get_patterns(node)
+ _apply_stroke_patterns(patterns['stroke_patterns'], patches)
+ _apply_fill_patterns(patterns['fill_patterns'], patches)
+
+
+def _apply_stroke_patterns(patterns, patches):
+ for pattern in patterns:
+ for patch in patches:
+ patch_points = []
+ for i, stitch in enumerate(patch.stitches):
+ patch_points.append(stitch)
+ if i == len(patch.stitches) - 1:
+ continue
+ intersection_points = _get_pattern_points(stitch, patch.stitches[i+1], pattern)
+ for point in intersection_points:
+ patch_points.append(Stitch(point, tags=('pattern_point',)))
+ patch.stitches = patch_points
+
+
+def _apply_fill_patterns(patterns, patches):
+ for pattern in patterns:
+ for patch in patches:
+ patch_points = []
+ for i, stitch in enumerate(patch.stitches):
+ if not shgeo.Point(stitch).within(pattern):
+ # keep points outside the fill pattern
+ patch_points.append(stitch)
+ elif i - 1 < 0 or i >= len(patch.stitches) - 1:
+ # keep start and end points
+ patch_points.append(stitch)
+ elif stitch.has_tag('fill_row_start') or stitch.has_tag('fill_row_end'):
+ # keep points if they are the start or end of a fill stitch row
+ patch_points.append(stitch)
+ elif stitch.has_tag('auto_fill') and not stitch.has_tag('auto_fill_top'):
+ # keep auto-fill underlay
+ patch_points.append(stitch)
+ elif stitch.has_tag('auto_fill_travel'):
+ # keep travel stitches (underpath or travel around the border)
+ patch_points.append(stitch)
+ elif stitch.has_tag('satin_column') and not stitch.has_tag('satin_split_stitch'):
+ # keep satin column stitches unless they are split stitches
+ patch_points.append(stitch)
+ patch.stitches = patch_points
+
+
+def _get_patterns(node):
+ from .elements import EmbroideryElement
+ from .elements.fill import Fill
+ from .elements.stroke import Stroke
+
+ fills = []
+ strokes = []
+ xpath = "./parent::svg:g/*[contains(@style, 'marker-start:url(#inkstitch-pattern-marker)')]"
+ patterns = node.xpath(xpath, namespaces=inkex.NSS)
+ for pattern in patterns:
+ if pattern.tag not in EMBROIDERABLE_TAGS:
+ continue
+
+ element = EmbroideryElement(pattern)
+ fill = element.get_style('fill')
+ stroke = element.get_style('stroke')
+
+ if fill is not None:
+ fill_pattern = Fill(pattern).shape
+ fills.append(fill_pattern)
+
+ if stroke is not None:
+ stroke_pattern = Stroke(pattern).paths
+ line_strings = [shgeo.LineString(path) for path in stroke_pattern]
+ strokes.append(shgeo.MultiLineString(line_strings))
+
+ return {'fill_patterns': fills, 'stroke_patterns': strokes}
+
+
+def _get_pattern_points(first, second, pattern):
+ points = []
+ intersection = shgeo.LineString([first, second]).intersection(pattern)
+ if isinstance(intersection, shgeo.Point):
+ points.append(Point(intersection.x, intersection.y))
+ if isinstance(intersection, shgeo.MultiPoint):
+ for point in intersection:
+ points.append(Point(point.x, point.y))
+ # sort points after their distance to first
+ points.sort(key=lambda point: point.distance(first))
+ return points
diff --git a/lib/stitch_plan/__init__.py b/lib/stitch_plan/__init__.py
index 68301e94..d4b43ace 100644
--- a/lib/stitch_plan/__init__.py
+++ b/lib/stitch_plan/__init__.py
@@ -3,6 +3,8 @@
# Copyright (c) 2010 Authors
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
-from .stitch_plan import patches_to_stitch_plan, StitchPlan, ColorBlock
+from .stitch_plan import stitch_groups_to_stitch_plan, StitchPlan
+from .color_block import ColorBlock
+from .stitch_group import StitchGroup
from .stitch import Stitch
from .read_file import stitch_plan_from_file
diff --git a/lib/stitch_plan/color_block.py b/lib/stitch_plan/color_block.py
new file mode 100644
index 00000000..86edaff2
--- /dev/null
+++ b/lib/stitch_plan/color_block.py
@@ -0,0 +1,143 @@
+# 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 .stitch import Stitch
+from ..threads import ThreadColor
+from ..utils.geometry import Point
+from ..svg import PIXELS_PER_MM
+
+
+class ColorBlock(object):
+ """Holds a set of stitches, all with the same thread color."""
+
+ def __init__(self, color=None, stitches=None):
+ self.color = color
+ self.stitches = stitches or []
+
+ def __iter__(self):
+ return iter(self.stitches)
+
+ def __len__(self):
+ return len(self.stitches)
+
+ def __repr__(self):
+ return "ColorBlock(%s, %s)" % (self.color, self.stitches)
+
+ def __getitem__(self, item):
+ return self.stitches[item]
+
+ def __delitem__(self, item):
+ del self.stitches[item]
+
+ def __json__(self):
+ return dict(color=self.color, stitches=self.stitches)
+
+ def has_color(self):
+ return self._color is not None
+
+ @property
+ def color(self):
+ return self._color
+
+ @color.setter
+ def color(self, value):
+ if isinstance(value, ThreadColor):
+ self._color = value
+ elif value is None:
+ self._color = None
+ else:
+ self._color = ThreadColor(value)
+
+ @property
+ def last_stitch(self):
+ if self.stitches:
+ return self.stitches[-1]
+ else:
+ return None
+
+ @property
+ def num_stitches(self):
+ """Number of stitches in this color block."""
+ return len(self.stitches)
+
+ @property
+ def num_trims(self):
+ """Number of trims in this color block."""
+
+ return sum(1 for stitch in self if stitch.trim)
+
+ @property
+ def stop_after(self):
+ if self.last_stitch is not None:
+ return self.last_stitch.stop
+ else:
+ return False
+
+ @property
+ def trim_after(self):
+ # If there's a STOP, it will be at the end. We still want to return
+ # True.
+ for stitch in reversed(self.stitches):
+ if stitch.stop or stitch.jump:
+ continue
+ elif stitch.trim:
+ return True
+ else:
+ break
+
+ return False
+
+ def filter_duplicate_stitches(self):
+ if not self.stitches:
+ return
+
+ stitches = [self.stitches[0]]
+
+ for stitch in self.stitches[1:]:
+ if stitches[-1].jump or stitch.stop or stitch.trim or stitch.color_change:
+ # Don't consider jumps, stops, color changes, or trims as candidates for filtering
+ pass
+ else:
+ length = (stitch - stitches[-1]).length()
+ if length <= 0.1 * PIXELS_PER_MM:
+ # duplicate stitch, skip this one
+ continue
+
+ stitches.append(stitch)
+
+ self.stitches = stitches
+
+ def add_stitch(self, *args, **kwargs):
+ if not args:
+ # They're adding a command, e.g. `color_block.add_stitch(stop=True)``.
+ # Use the position from the last stitch.
+ if self.last_stitch:
+ args = (self.last_stitch.x, self.last_stitch.y)
+ else:
+ raise ValueError("internal error: can't add a command to an empty stitch block")
+ self.stitches.append(Stitch(*args, **kwargs))
+ if isinstance(args[0], Stitch):
+ self.stitches.append(args[0])
+ elif isinstance(args[0], Point):
+ self.stitches.append(Stitch(args[0].x, args[0].y, *args[1:], **kwargs))
+
+ def add_stitches(self, stitches, *args, **kwargs):
+ for stitch in stitches:
+ if isinstance(stitch, (Stitch, Point)):
+ self.add_stitch(stitch, *args, **kwargs)
+ else:
+ self.add_stitch(*stitch, *args, **kwargs)
+
+ def replace_stitches(self, stitches):
+ self.stitches = stitches
+
+ @property
+ def bounding_box(self):
+ minx = min(stitch.x for stitch in self)
+ miny = min(stitch.y for stitch in self)
+ maxx = max(stitch.x for stitch in self)
+ maxy = max(stitch.y for stitch in self)
+
+ return minx, miny, maxx, maxy
diff --git a/lib/stitch_plan/stitch.py b/lib/stitch_plan/stitch.py
index ae6fa480..f163d09c 100644
--- a/lib/stitch_plan/stitch.py
+++ b/lib/stitch_plan/stitch.py
@@ -4,12 +4,25 @@
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
from ..utils.geometry import Point
+from copy import deepcopy
class Stitch(Point):
- def __init__(self, x, y=None, color=None, jump=False, stop=False, trim=False, color_change=False, tie_modus=0, no_ties=False):
- self.x = x
- self.y = y
+ """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, no_ties=False, tags=None):
+ if isinstance(x, Stitch):
+ # Allow creating a Stitch from another Stitch. Attributes passed as
+ # arguments will override any existing attributes.
+ vars(self).update(deepcopy(vars(x)))
+ elif isinstance(x, Point):
+ # Allow creating a Stitch from a Point
+ point = x
+ self.x = point.x
+ self.y = point.y
+ else:
+ Point.__init__(self, x, y)
+
self.color = color
self.jump = jump
self.trim = trim
@@ -17,12 +30,9 @@ class Stitch(Point):
self.color_change = color_change
self.tie_modus = tie_modus
self.no_ties = no_ties
+ self.tags = set()
- # Allow creating a Stitch from a Point
- if isinstance(x, Point):
- point = x
- self.x = point.x
- self.y = point.y
+ self.add_tags(tags or [])
def __repr__(self):
return "Stitch(%s, %s, %s, %s, %s, %s, %s, %s, %s)" % (self.x,
@@ -35,8 +45,32 @@ class Stitch(Point):
"NO TIES" if self.no_ties else " ",
"COLOR CHANGE" if self.color_change else " ")
+ def add_tags(self, tags):
+ for tag in tags:
+ self.add_tag(tag)
+
+ def add_tag(self, tag):
+ """Store arbitrary information about a stitch.
+
+ Tags can be used to store any information about a stitch. This can be
+ used by other parts of the code to keep track of where a Stitch came
+ from. The Stitch treats tags as opaque.
+
+ Use strings as tags. Python automatically optimizes this kind of
+ usage of strings, and it doesn't have to constantly do string
+ comparisons. More details here:
+
+ https://stackabuse.com/guide-to-string-interning-in-python
+ """
+ self.tags.add(tag)
+
+ def has_tag(self, tag):
+ 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.no_ties)
+ return Stitch(self.x, self.y, self.color, self.jump, self.stop, self.trim, self.color_change, self.tie_modus, self.no_ties, self.tags)
def __json__(self):
- return vars(self)
+ attributes = dict(vars(self))
+ attributes['tags'] = list(attributes['tags'])
+ return attributes
diff --git a/lib/stitch_plan/stitch_group.py b/lib/stitch_plan/stitch_group.py
new file mode 100644
index 00000000..98d9799e
--- /dev/null
+++ b/lib/stitch_plan/stitch_group.py
@@ -0,0 +1,64 @@
+# 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 .stitch import Stitch
+
+
+class StitchGroup:
+ """A collection of Stitch objects with attached instructions and attributes.
+
+ StitchGroups will later be combined to make ColorBlocks, which in turn are
+ combined to make a StitchPlan. Jump stitches are allowed between
+ StitchGroups, but not between stitches inside a StitchGroup. This means
+ that EmbroideryElement classes should produce multiple StitchGroups only if
+ they want to allow for the possibility of jump stitches to be added in
+ between them by the stitch plan generation code.
+ """
+
+ def __init__(self, color=None, stitches=None, trim_after=False, stop_after=False, tie_modus=0, stitch_as_is=False, tags=None):
+ self.color = color
+ self.trim_after = trim_after
+ self.stop_after = stop_after
+ self.tie_modus = tie_modus
+ self.stitch_as_is = stitch_as_is
+ self.stitches = []
+
+ if stitches:
+ self.add_stitches(stitches)
+
+ if tags:
+ self.add_tags(tags)
+
+ def __add__(self, other):
+ if isinstance(other, StitchGroup):
+ return StitchGroup(self.color, self.stitches + other.stitches)
+ else:
+ raise TypeError("StitchGroup can only be added to another StitchGroup")
+
+ def __len__(self):
+ # This method allows `len(patch)` and `if patch:
+ return len(self.stitches)
+
+ def add_stitches(self, stitches):
+ for stitch in stitches:
+ self.add_stitch(stitch)
+
+ def add_stitch(self, stitch):
+ if not isinstance(stitch, Stitch):
+ # probably a Point
+ stitch = Stitch(stitch)
+
+ self.stitches.append(stitch)
+
+ def reverse(self):
+ return StitchGroup(self.color, self.stitches[::-1])
+
+ def add_tags(self, tags):
+ for stitch in self.stitches:
+ stitch.add_tags(tags)
+
+ def add_tag(self, tag):
+ for stitch in self.stitches:
+ stitch.add_tag(tag)
diff --git a/lib/stitch_plan/stitch_plan.py b/lib/stitch_plan/stitch_plan.py
index fc0d3760..7e7621c1 100644
--- a/lib/stitch_plan/stitch_plan.py
+++ b/lib/stitch_plan/stitch_plan.py
@@ -3,56 +3,54 @@
# Copyright (c) 2010 Authors
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
-from ..svg import PIXELS_PER_MM
-from ..threads import ThreadColor
-from ..utils.geometry import Point
-from .stitch import Stitch
from .ties import add_ties
+from .color_block import ColorBlock
+from ..svg import PIXELS_PER_MM
-def patches_to_stitch_plan(patches, collapse_len=None, disable_ties=False): # noqa: C901
+def stitch_groups_to_stitch_plan(stitch_groups, collapse_len=None, disable_ties=False): # noqa: C901
- """Convert a collection of inkstitch.element.Patch objects to a StitchPlan.
+ """Convert a collection of StitchGroups to a StitchPlan.
- * applies instructions embedded in the Patch such as trim_after and stop_after
+ * applies instructions embedded in the StitchGroup such as trim_after and stop_after
* adds tie-ins and tie-offs
- * adds jump-stitches between patches if necessary
+ * adds jump-stitches between stitch_group if necessary
"""
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=patches[0].color)
+ color_block = stitch_plan.new_color_block(color=stitch_groups[0].color)
- for patch in patches:
- if not patch.stitches:
+ for stitch_group in stitch_groups:
+ if not stitch_group.stitches:
continue
- if color_block.color != patch.color:
+ if color_block.color != stitch_group.color:
if len(color_block) == 0:
# We just processed a stop, which created a new color block.
# We'll just claim this new block as ours:
- color_block.color = patch.color
+ color_block.color = stitch_group.color
else:
# 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=patch.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(patch.stitches[0], jump=True)
+ color_block.add_stitch(stitch_group.stitches[0], jump=True)
else:
- if len(color_block) and (patch.stitches[0] - color_block.stitches[-1]).length() > collapse_len:
- color_block.add_stitch(patch.stitches[0], jump=True)
+ if len(color_block) and (stitch_group.stitches[0] - color_block.stitches[-1]).length() > collapse_len:
+ color_block.add_stitch(stitch_group.stitches[0], jump=True)
- color_block.add_stitches(stitches=patch.stitches, tie_modus=patch.tie_modus, no_ties=patch.stitch_as_is)
+ color_block.add_stitches(stitches=stitch_group.stitches, tie_modus=stitch_group.tie_modus, no_ties=stitch_group.stitch_as_is)
- if patch.trim_after:
+ if stitch_group.trim_after:
color_block.add_stitch(trim=True)
- if patch.stop_after:
+ if stitch_group.stop_after:
color_block.add_stitch(stop=True)
color_block = stitch_plan.new_color_block(color_block.color)
@@ -168,141 +166,3 @@ class StitchPlan(object):
return self.color_blocks[-1]
else:
return None
-
-
-class ColorBlock(object):
- """Holds a set of stitches, all with the same thread color."""
-
- def __init__(self, color=None, stitches=None):
- self.color = color
- self.stitches = stitches or []
-
- def __iter__(self):
- return iter(self.stitches)
-
- def __len__(self):
- return len(self.stitches)
-
- def __repr__(self):
- return "ColorBlock(%s, %s)" % (self.color, self.stitches)
-
- def __getitem__(self, item):
- return self.stitches[item]
-
- def __delitem__(self, item):
- del self.stitches[item]
-
- def __json__(self):
- return dict(color=self.color, stitches=self.stitches)
-
- def has_color(self):
- return self._color is not None
-
- @property
- def color(self):
- return self._color
-
- @color.setter
- def color(self, value):
- if isinstance(value, ThreadColor):
- self._color = value
- elif value is None:
- self._color = None
- else:
- self._color = ThreadColor(value)
-
- @property
- def last_stitch(self):
- if self.stitches:
- return self.stitches[-1]
- else:
- return None
-
- @property
- def num_stitches(self):
- """Number of stitches in this color block."""
- return len(self.stitches)
-
- @property
- def num_trims(self):
- """Number of trims in this color block."""
-
- return sum(1 for stitch in self if stitch.trim)
-
- @property
- def stop_after(self):
- if self.last_stitch is not None:
- return self.last_stitch.stop
- else:
- return False
-
- @property
- def trim_after(self):
- # If there's a STOP, it will be at the end. We still want to return
- # True.
- for stitch in reversed(self.stitches):
- if stitch.stop or stitch.jump:
- continue
- elif stitch.trim:
- return True
- else:
- break
-
- return False
-
- def filter_duplicate_stitches(self):
- if not self.stitches:
- return
-
- stitches = [self.stitches[0]]
-
- for stitch in self.stitches[1:]:
- if stitches[-1].jump or stitch.stop or stitch.trim or stitch.color_change:
- # Don't consider jumps, stops, color changes, or trims as candidates for filtering
- pass
- else:
- length = (stitch - stitches[-1]).length()
- if length <= 0.1 * PIXELS_PER_MM:
- # duplicate stitch, skip this one
- continue
-
- stitches.append(stitch)
-
- self.stitches = stitches
-
- def add_stitch(self, *args, **kwargs):
- if not args:
- # They're adding a command, e.g. `color_block.add_stitch(stop=True)``.
- # Use the position from the last stitch.
- if self.last_stitch:
- args = (self.last_stitch.x, self.last_stitch.y)
- else:
- raise ValueError("internal error: can't add a command to an empty stitch block")
-
- if isinstance(args[0], Stitch):
- self.stitches.append(args[0])
- elif isinstance(args[0], Point):
- self.stitches.append(Stitch(args[0].x, args[0].y, *args[1:], **kwargs))
- else:
- if not args and self.last_stitch:
- args = (self.last_stitch.x, self.last_stitch.y)
- self.stitches.append(Stitch(*args, **kwargs))
-
- def add_stitches(self, stitches, *args, **kwargs):
- for stitch in stitches:
- if isinstance(stitch, (Stitch, Point)):
- self.add_stitch(stitch, *args, **kwargs)
- else:
- self.add_stitch(*(list(stitch) + args), **kwargs)
-
- def replace_stitches(self, stitches):
- self.stitches = stitches
-
- @property
- def bounding_box(self):
- minx = min(stitch.x for stitch in self)
- miny = min(stitch.y for stitch in self)
- maxx = max(stitch.x for stitch in self)
- maxy = max(stitch.y for stitch in self)
-
- return minx, miny, maxx, maxy
diff --git a/lib/stitches/auto_fill.py b/lib/stitches/auto_fill.py
index 07361f13..160d927e 100644
--- a/lib/stitches/auto_fill.py
+++ b/lib/stitches/auto_fill.py
@@ -14,6 +14,7 @@ from shapely.ops import snap
from shapely.strtree import STRtree
from ..debug import debug
+from ..stitch_plan import Stitch
from ..svg import PIXELS_PER_MM
from ..utils.geometry import Point as InkstitchPoint
from ..utils.geometry import line_string_to_point_list
@@ -592,9 +593,12 @@ def travel(travel_graph, start, end, running_stitch_length, skip_last):
"""Create stitches to get from one point on an outline of the shape to another."""
path = networkx.shortest_path(travel_graph, start, end, weight='weight')
- path = [InkstitchPoint(*p) for p in path]
+ path = [Stitch(*p) for p in path]
stitches = running_stitch(path, running_stitch_length)
+ for stitch in stitches:
+ stitch.add_tag('auto_fill_travel')
+
# The path's first stitch will start at the end of a row of stitches. We
# don't want to double that last stitch, so we'd like to skip it.
if skip_last and len(path) > 2:
@@ -619,7 +623,7 @@ def path_to_stitches(path, travel_graph, fill_stitch_graph, angle, row_spacing,
# If the very first stitch is travel, we'll omit it in travel(), so add it here.
if not path[0].is_segment():
- stitches.append(InkstitchPoint(*path[0].nodes[0]))
+ stitches.append(Stitch(*path[0].nodes[0]))
for edge in path:
if edge.is_segment():
diff --git a/lib/stitches/fill.py b/lib/stitches/fill.py
index 5aead9c7..d134be32 100644
--- a/lib/stitches/fill.py
+++ b/lib/stitches/fill.py
@@ -7,6 +7,7 @@ import math
import shapely
+from ..stitch_plan import Stitch
from ..svg import PIXELS_PER_MM
from ..utils import Point as InkstitchPoint
from ..utils import cache
@@ -65,16 +66,13 @@ def stitch_row(stitches, beg, end, angle, row_spacing, max_stitch_length, stagge
# tile with each other. That's important because we often get
# abutting fill regions from pull_runs().
- beg = InkstitchPoint(*beg)
- end = InkstitchPoint(*end)
+ beg = Stitch(*beg, tags=('fill_row_start',))
+ end = Stitch(*end, tags=('fill_row_end',))
row_direction = (end - beg).unit()
segment_length = (end - beg).length()
- # only stitch the first point if it's a reasonable distance away from the
- # last stitch
- if not stitches or (beg - stitches[-1]).length() > 0.5 * PIXELS_PER_MM:
- stitches.append(beg)
+ stitches.append(beg)
first_stitch = adjust_stagger(beg, angle, row_spacing, max_stitch_length, staggers)
@@ -85,7 +83,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(beg + offset * row_direction)
+ 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/utils/geometry.py b/lib/utils/geometry.py
index 1e0c6ad0..bce278ed 100644
--- a/lib/utils/geometry.py
+++ b/lib/utils/geometry.py
@@ -73,22 +73,22 @@ class Point:
return vars(self)
def __add__(self, other):
- return Point(self.x + other.x, self.y + other.y)
+ return self.__class__(self.x + other.x, self.y + other.y)
def __sub__(self, other):
- return Point(self.x - other.x, self.y - other.y)
+ return self.__class__(self.x - other.x, self.y - other.y)
def mul(self, scalar):
- return Point(self.x * scalar, self.y * scalar)
+ return self.__class__(self.x * scalar, self.y * scalar)
def __mul__(self, other):
if isinstance(other, Point):
# dot product
return self.x * other.x + self.y * other.y
elif isinstance(other, (int, float)):
- return Point(self.x * other, self.y * other)
+ return self.__class__(self.x * other, self.y * other)
else:
- raise ValueError("cannot multiply Point by %s" % type(other))
+ raise ValueError("cannot multiply %s by %s" % (type(self), type(other)))
def __neg__(self):
return self * -1
@@ -97,16 +97,16 @@ class Point:
if isinstance(other, (int, float)):
return self.__mul__(other)
else:
- raise ValueError("cannot multiply Point by %s" % type(other))
+ raise ValueError("cannot multiply %s by %s" % (type(self), type(other)))
def __div__(self, other):
if isinstance(other, (int, float)):
return self * (1.0 / other)
else:
- raise ValueError("cannot divide Point by %s" % type(other))
+ raise ValueError("cannot divide %s by %s" % (type(self), type(other)))
def __repr__(self):
- return "Point(%s,%s)" % (self.x, self.y)
+ return "%s(%s,%s)" % (type(self), self.x, self.y)
def length(self):
return math.sqrt(math.pow(self.x, 2.0) + math.pow(self.y, 2.0))
@@ -118,13 +118,13 @@ class Point:
return self.mul(1.0 / self.length())
def rotate_left(self):
- return Point(-self.y, self.x)
+ return self.__class__(-self.y, self.x)
def rotate(self, angle):
- return Point(self.x * math.cos(angle) - self.y * math.sin(angle), self.y * math.cos(angle) + self.x * math.sin(angle))
+ return self.__class__(self.x * math.cos(angle) - self.y * math.sin(angle), self.y * math.cos(angle) + self.x * math.sin(angle))
def as_int(self):
- return Point(int(round(self.x)), int(round(self.y)))
+ return self.__class__(int(round(self.x)), int(round(self.y)))
def as_tuple(self):
return (self.x, self.y)
diff --git a/templates/selection_to_pattern.xml b/templates/selection_to_pattern.xml
new file mode 100644
index 00000000..859a51ed
--- /dev/null
+++ b/templates/selection_to_pattern.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
+ <name>{% trans %}Selection to pattern{% endtrans %}</name>
+ <id>org.inkstitch.selection_to_pattern.{{ locale }}</id>
+ <param name="extension" type="string" gui-hidden="true">selection_to_pattern</param>
+ <effect>
+ <object-type>all</object-type>
+ <effects-menu>
+ <submenu name="Ink/Stitch">
+ <submenu name="{% trans %}Edit{% endtrans %}" />
+ </submenu>
+ </effects-menu>
+ </effect>
+ <script>
+ {{ command_tag | safe }}
+ </script>
+</inkscape-extension>