from __future__ import annotations
import itertools
import math
from abc import ABC, abstractmethod
from copy import copy
from typing import Any, Generator, Iterable
try:
from typing import Literal, Self, TypeAlias
except ImportError:
from typing_extensions import Literal, Self, TypeAlias
import pyembroidery
from .base_turtle import Vec2D
# STITCH=0, JUMP=1, TRIM=2
StitchCommand: TypeAlias = Literal[0, 1, 2]
[docs]
class EmbroideryPattern:
"""Abstract representation of an embroidery pattern.
Container object
Parameters
----------
scale: int (optional, default=1)
All coordinates are multiplied by this parameter before converting it to a PyEmbroidery pattern.
This is useful to control the number of steps per mm (default is 10 steps per mm).
"""
def __init__(self, scale: int = 1) -> None:
self.stitch_groups: list[StitchGroup | EmbroideryPattern] = []
self.scale = scale
[docs]
def to_pyembroidery(self) -> pyembroidery.EmbPattern:
"""Convert to a PyEmbroidery pattern."""
pattern = pyembroidery.EmbPattern()
for stitch_group in self.stitch_groups:
scaled_stitch_commands = (
(x * self.scale, y * self.scale, cmd) for x, y, cmd in stitch_group.get_stitch_commands()
)
pattern.stitches.extend(scaled_stitch_commands)
return pattern
[docs]
def get_stitch_command(self) -> list[tuple[float, float, StitchCommand]]:
"""Get stitch commands for PyEmbroidery.
This function is used when embroidery patterns contain whole embroidery patterns. If you're not explicitly
making patterns within patterns, then you probably want to use the :py:meth:`to_pyembroidery` method instead.
"""
for stitch_group in self.stitch_groups:
yield from stitch_group.get_stitch_commands()
[docs]
class StitchGroup(ABC):
"""Object representing one contiguous set of commands for the embroidery machine.
Stitch groups are used to convert the Turtle commands into embroidery machine commands. For example, if you want to
embroider with a running stitch, then you'd create a stitch group for a running stitch with the corresponding
stitch length.
Stitch groups work by storing subsequent locations of the Turtle and converts them into embroidery commands.
Parameters
----------
start_pos: Vec2D (tuple[float, float])
The initial position of the turtle.
"""
def __init__(self, start_pos: Vec2D) -> None:
self._start_pos = start_pos
self._positions = []
self._stitch_commands = None
self._parent_stitch_group = self
[docs]
def add_location(self, location: Vec2D) -> None:
"""Add a new location to this stitch group."""
self._stitch_commands = None
self._positions.append(location)
@abstractmethod
def _get_stitch_commands(self) -> list[tuple[float, float, StitchCommand]]:
raise NotImplementedError
[docs]
def get_stitch_commands(self) -> list[tuple[float, float, StitchCommand]]:
"""Get the list of PyEmbroidery stitch commands for this stitch group"""
if self._stitch_commands is None:
self._stitch_commands = self._get_stitch_commands()
return self._stitch_commands.copy()
[docs]
def empty_copy(self, start_pos) -> Self:
"""Create a copy of the stitch group but with no stored locations (i.e. no stitches)."""
copied_group = copy(self)
copied_group._positions = []
copied_group._start_pos = start_pos
copied_group._parent_stitch_group = self._parent_stitch_group
return copied_group
[docs]
class RunningStitch(StitchGroup):
"""Stitch group for running stitches.
With a running stitch, we get stitches with a constant distance between each stitch.
If the turtle is supposed to move a number of steps that is not a multiple of ``stitch_length``, then all but the
last stitch in that stretch will have the same length and the last stitch will be between ``0.5*stitch_length`` and
``1.5*stitch_length``.
Parameters
----------
stitch_length : int
Number of steps between each stitch.
"""
def __init__(self, start_pos: Vec2D, stitch_length: int | float) -> None:
super().__init__(start_pos=start_pos)
self.stitch_length = stitch_length
def _iter_stitches_between_positions(
self, position_1: Vec2D, position_2: Vec2D
) -> Generator[tuple[StitchCommand, float, float], None, None]:
# Running stitch between two points, stopping exactly at position 2 and not
# adding any stitch at position 1. The final stitch will be between 0.5 and 1.5
# times the stitch length.
x, y = position_1
x_end, y_end = position_2
distance = math.sqrt((x - x_end) ** 2 + (y - y_end) ** 2)
angle = math.atan2(y_end - y, x_end - x)
dx = math.cos(angle)
dy = math.sin(angle)
# First, the needle does stitches until the distance to the end-point is
# less than two stitch-lengths away
distance_traveled = 0
while distance_traveled + 2 * self.stitch_length < distance:
x += self.stitch_length * dx
y += self.stitch_length * dy
distance_traveled += self.stitch_length
yield x, y, pyembroidery.STITCH
# Then, we check if we need one final stitch, to prevent stitches larger than
# 1.5 times the stitch length
if distance - distance_traveled >= 1.5 * self.stitch_length:
x += self.stitch_length * dx
y += self.stitch_length * dy
distance_traveled += self.stitch_length
yield x, y, pyembroidery.STITCH
# We add the final stitch at the end-point, which is guaranteed to be at most
# 1.5 and at least 0.5 stitch-lengths away from the second to last stitch.
yield x_end, y_end, pyembroidery.STITCH
def _get_stitch_commands(self) -> list[tuple[float, float, StitchCommand]]:
if not self._positions:
return []
stitch_commands = [(self._start_pos[0], self._start_pos[1], pyembroidery.STITCH)]
stitch_commands.extend(self._iter_stitches_between_positions(self._start_pos, self._positions[0]))
for pos1, pos2 in itertools.pairwise(self._positions):
stitch_commands.extend(self._iter_stitches_between_positions(pos1, pos2))
return stitch_commands
def iterate_back_and_forth(iterable: Iterable[Any]) -> Generator[tuple[StitchCommand, float, float], None, None]:
"""Iterates back and forth trough an iterable
Each element (except the first) is given twice, with the previous element sandwiched inbetween.
(So all element exept he first and last is given in total three times)
Parameters
----------
iterable
Iterable to iterate back and forth over
Yields
------
Elements from the iterable
>>> list(iterate_back_and_forth([0, 1, 2, 3]))
[0, 1, 0, 1, 2, 1, 2, 3, 2, 3]
"""
iterator = iter(iterable)
previous = next(iterator)
yield previous
for item in iterator:
yield item
yield previous
yield item
previous = item
class TripleStitch(StitchGroup):
"""Stitch group for triple stitches.
TripleStitch is the same as a :py:class:`RunningStitch`, but the thread moves back and forth three times for each
stitch.
Parameters
----------
stitch_length : int
Number of steps between each stitch.
"""
def __init__(self, start_pos: Vec2D, stitch_length: float) -> None:
super().__init__(start_pos=start_pos)
self.running_stitch = RunningStitch(start_pos=start_pos, stitch_length=stitch_length)
def _get_stitch_commands(self) -> list[tuple[float, float, StitchCommand]]:
self.running_stitch._positions = self._positions
stitch_commands = self.running_stitch._get_stitch_commands()
return list(iterate_back_and_forth(stitch_commands))
[docs]
class JumpStitch(StitchGroup):
"""Stitch group for jump stitches.
A jump stitch group always starts with a trim command followed by the needle moving without sewing any stitches.
See :py:class:`StitchGroup` for more information on stitch groups.
Parameters
----------
skip_intermediate_jumps : bool (optional, default=True)
If True, then multiple jump commands will be collapsed into one jump command. This is useful in the cases
where there may be multiple subsequent jumps with no stitches inbetween. Multiple subsequent jumps doesn't
make sense but it can happen dependent on how you generate your patterns.
"""
def __init__(self, start_pos: Vec2D, skip_intermediate_jumps: bool = True) -> None:
super().__init__(start_pos=start_pos)
self.skip_intermediate_jumps = skip_intermediate_jumps
def _get_stitch_commands(self) -> list[tuple[float, float, StitchCommand]]:
if not self._positions:
return []
stitch_commands = [(self._start_pos[0], self._start_pos[1], pyembroidery.TRIM)]
if self.skip_intermediate_jumps:
x, y = self._positions[-1]
stitch_commands.append((x, y, pyembroidery.JUMP))
return stitch_commands
for x, y in self._positions:
stitch_commands.append((x, y, pyembroidery.JUMP))
return stitch_commands