kopia lustrzana https://github.com/inkstitch/inkstitch
Merge pull request #1254 from inkstitch/kaalleen/satin-patterns
Satin pattern and split stitchpull/1322/head
commit
3ebc238561
|
@ -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)
|
||||
|
|
|
@ -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)"),
|
||||
|
||||
|
|
|
@ -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]
|
||||
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)
|
||||
|
||||
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))
|
||||
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)
|
||||
|
||||
if self.shrink_or_grow_shape(self.expand, True).is_empty:
|
||||
yield ExpandWarning(self.shape.centroid)
|
||||
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.fill_underlay_inset, True).is_empty:
|
||||
yield UnderlayInsetWarning(self.shape.centroid)
|
||||
if self.shrink_or_grow_shape(self.expand, True).is_empty:
|
||||
yield ExpandWarning(self.shape.centroid)
|
||||
|
||||
for warning in super(AutoFill, self).validation_warnings():
|
||||
yield warning
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 []
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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 []
|
||||
|
|
|
@ -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 []
|
|
@ -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))
|
||||
|
|
|
@ -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):
|
||||
|
@ -80,6 +81,14 @@ class SatinColumn(EmbroideryElement):
|
|||
def e_stitch(self):
|
||||
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 to_patches(self, last_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 _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()
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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 []
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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', [])
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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))
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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().
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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)
|
|
@ -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
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
Ładowanie…
Reference in New Issue