summaryrefslogtreecommitdiff
path: root/lib/extensions
diff options
context:
space:
mode:
Diffstat (limited to 'lib/extensions')
-rw-r--r--lib/extensions/base.py5
-rw-r--r--lib/extensions/convert_to_satin.py71
-rw-r--r--lib/extensions/gradient_blocks.py7
-rw-r--r--lib/extensions/letters_to_font.py18
-rw-r--r--lib/extensions/object_commands_toggle_visibility.py2
-rw-r--r--lib/extensions/params.py20
-rw-r--r--lib/extensions/preferences.py26
-rw-r--r--lib/extensions/remove_embroidery_settings.py67
-rw-r--r--lib/extensions/stroke_to_lpe_satin.py2
-rw-r--r--lib/extensions/zigzag_line_to_satin.py16
-rw-r--r--lib/extensions/zip.py39
11 files changed, 171 insertions, 102 deletions
diff --git a/lib/extensions/base.py b/lib/extensions/base.py
index 3c16a11c..e0bf4131 100644
--- a/lib/extensions/base.py
+++ b/lib/extensions/base.py
@@ -7,7 +7,6 @@ import os
import inkex
from lxml.etree import Comment
-from stringcase import snakecase
from ..commands import is_command, layer_commands
from ..elements import EmbroideryElement, nodes_to_elements
@@ -32,7 +31,9 @@ class InkstitchExtension(inkex.EffectExtension):
@classmethod
def name(cls):
- return snakecase(cls.__name__)
+ # Convert CamelCase to snake_case
+ return cls.__name__[0].lower() + ''.join([x if x.islower() else f'_{x.lower()}'
+ for x in cls.__name__[1:]])
def hide_all_layers(self):
for g in self.document.getroot().findall(SVG_GROUP_TAG):
diff --git a/lib/extensions/convert_to_satin.py b/lib/extensions/convert_to_satin.py
index 7a36ce21..4bb3588e 100644
--- a/lib/extensions/convert_to_satin.py
+++ b/lib/extensions/convert_to_satin.py
@@ -13,7 +13,7 @@ from numpy import diff, setdiff1d, sign
from shapely import geometry as shgeo
from .base import InkstitchExtension
-from ..elements import Stroke
+from ..elements import SatinColumn, Stroke
from ..i18n import _
from ..svg import PIXELS_PER_MM, get_correction_transform
from ..svg.tags import INKSTITCH_ATTRIBS
@@ -51,22 +51,28 @@ class ConvertToSatin(InkstitchExtension):
path_style = self.path_style(element)
for path in element.paths:
- path = self.remove_duplicate_points(path)
+ path = self.remove_duplicate_points(self.fix_loop(path))
if len(path) < 2:
# ignore paths with just one point -- they're not visible to the user anyway
continue
- for satin in self.convert_path_to_satins(path, element.stroke_width, style_args, correction_transform, path_style):
- parent.insert(index, satin)
- index += 1
+ satins = list(self.convert_path_to_satins(path, element.stroke_width, style_args, path_style))
+
+ if satins:
+ joined_satin = satins[0]
+ for satin in satins[1:]:
+ joined_satin = joined_satin.merge(satin)
+
+ joined_satin.node.set('transform', correction_transform)
+ parent.insert(index, joined_satin.node)
parent.remove(element.node)
- def convert_path_to_satins(self, path, stroke_width, style_args, correction_transform, path_style, depth=0):
+ def convert_path_to_satins(self, path, stroke_width, style_args, path_style, depth=0):
try:
rails, rungs = self.path_to_satin(path, stroke_width, style_args)
- yield self.satin_to_svg_node(rails, rungs, correction_transform, path_style)
+ yield SatinColumn(self.satin_to_svg_node(rails, rungs, path_style))
except SelfIntersectionError:
# The path intersects itself. Split it in two and try doing the halves
# individually.
@@ -76,27 +82,37 @@ class ConvertToSatin(InkstitchExtension):
# getting nowhere. Just give up on this section of the path.
return
- half = int(len(path) / 2.0)
- halves = [path[:half + 1], path[half:]]
+ halves = self.split_path(path)
for path in halves:
- for satin in self.convert_path_to_satins(path, stroke_width, style_args, correction_transform, path_style, depth=depth + 1):
+ for satin in self.convert_path_to_satins(path, stroke_width, style_args, path_style, depth=depth + 1):
yield satin
+ def split_path(self, path):
+ half = len(path) // 2
+ halves = [path[:half], path[half:]]
+
+ start = Point.from_tuple(halves[0][-1])
+ end = Point.from_tuple(halves[1][0])
+
+ midpoint = (start + end) / 2
+ midpoint = midpoint.as_tuple()
+
+ halves[0].append(midpoint)
+ halves[1] = [midpoint] + halves[1]
+
+ return halves
+
def fix_loop(self, path):
- if path[0] == path[-1]:
- # Looping paths seem to confuse shapely's parallel_offset(). It loses track
- # of where the start and endpoint is, even if the user explicitly breaks the
- # path. I suspect this is because parallel_offset() uses buffer() under the
- # hood.
- #
- # To work around this we'll introduce a tiny gap by nudging the starting point
- # toward the next point slightly.
- start = Point(*path[0])
- next = Point(*path[1])
- direction = (next - start).unit()
- start += 0.01 * direction
- path[0] = start.as_tuple()
+ if path[0] == path[-1] and len(path) > 1:
+ first = Point.from_tuple(path[0])
+ second = Point.from_tuple(path[1])
+ midpoint = (first + second) / 2
+ midpoint = midpoint.as_tuple()
+
+ return [midpoint] + path[1:] + [path[0], midpoint]
+ else:
+ return path
def remove_duplicate_points(self, path):
path = [[round(coord, 4) for coord in point] for point in path]
@@ -304,10 +320,8 @@ class ConvertToSatin(InkstitchExtension):
# Rotate 90 degrees left to make a normal vector.
normal = tangent.rotate_left()
- # Travel 75% of the stroke width left and right to make the rung's
- # endpoints. This means the rung's length is 150% of the stroke
- # width.
- offset = normal * stroke_width * 0.75
+ # Extend the rungs by an offset value to make sure they will cross the rails
+ offset = normal * (stroke_width / 2) * 1.2
rung_start = rung_center + offset
rung_end = rung_center - offset
@@ -319,7 +333,7 @@ class ConvertToSatin(InkstitchExtension):
color = element.get_style('stroke', '#000000')
return "stroke:%s;stroke-width:1px;fill:none" % (color)
- def satin_to_svg_node(self, rails, rungs, correction_transform, path_style):
+ def satin_to_svg_node(self, rails, rungs, path_style):
d = ""
for path in chain(rails, rungs):
d += "M"
@@ -330,7 +344,6 @@ class ConvertToSatin(InkstitchExtension):
return inkex.PathElement(attrib={
"id": self.uniqueId("path"),
"style": path_style,
- "transform": correction_transform,
"d": d,
INKSTITCH_ATTRIBS['satin_column']: "true",
})
diff --git a/lib/extensions/gradient_blocks.py b/lib/extensions/gradient_blocks.py
index b8142d7f..f94582f0 100644
--- a/lib/extensions/gradient_blocks.py
+++ b/lib/extensions/gradient_blocks.py
@@ -52,7 +52,7 @@ class GradientBlocks(CommandsExtension):
correction_transform = get_correction_transform(element.node)
style = element.node.style
index = parent.index(element.node)
- fill_shapes, attributes = gradient_shapes_and_attributes(element, element.shape)
+ fill_shapes, attributes = gradient_shapes_and_attributes(element, element.shape, self.svg.viewport_to_unit(1))
# reverse order so we can always insert with the same index number
fill_shapes.reverse()
attributes.reverse()
@@ -127,7 +127,7 @@ class GradientBlocks(CommandsExtension):
return path
-def gradient_shapes_and_attributes(element, shape):
+def gradient_shapes_and_attributes(element, shape, unit_multiplier):
# e.g. url(#linearGradient872) -> linearGradient872
color = element.color[5:-1]
xpath = f'.//svg:defs/svg:linearGradient[@id="{color}"]'
@@ -144,6 +144,7 @@ def gradient_shapes_and_attributes(element, shape):
# create bbox polygon to calculate the length necessary to make sure that
# the gradient splitter lines will cut the entire design
+ # bounding_box returns the value in viewport units, we need to convert the length later to px
bbox = element.node.bounding_box()
bbox_polygon = shgeo.Polygon([(bbox.left, bbox.top), (bbox.right, bbox.top),
(bbox.right, bbox.bottom), (bbox.left, bbox.bottom)])
@@ -159,7 +160,7 @@ def gradient_shapes_and_attributes(element, shape):
for i, offset in enumerate(offsets):
shape_rest = []
split_point = shgeo.Point(line.point_at_ratio(float(offset)))
- length = split_point.hausdorff_distance(bbox_polygon)
+ length = split_point.hausdorff_distance(bbox_polygon) / unit_multiplier
split_line = shgeo.LineString([(split_point.x - length - 2, split_point.y),
(split_point.x + length + 2, split_point.y)])
split_line = rotate(split_line, angle, origin=split_point, use_radians=True)
diff --git a/lib/extensions/letters_to_font.py b/lib/extensions/letters_to_font.py
index 56a33ad8..d4d9e60a 100644
--- a/lib/extensions/letters_to_font.py
+++ b/lib/extensions/letters_to_font.py
@@ -39,6 +39,7 @@ class LettersToFont(InkstitchExtension):
glyphs = list(Path(font_dir).rglob(file_format.lower()))
document = self.document.getroot()
+ group = None
for glyph in glyphs:
letter = self.get_glyph_element(glyph)
label = "GlyphLayer-%s" % letter.get(INKSCAPE_LABEL, ' ').split('.')[0][-1]
@@ -59,15 +60,20 @@ class LettersToFont(InkstitchExtension):
document.insert(0, group)
group.set('style', 'display:none')
+ # We found no glyphs, no need to proceed
+ if group is None:
+ return
+
# users may be confused if they get an empty document
# make last letter visible again
group.set('style', None)
- # In most cases trims are inserted with the imported letters.
- # Let's make sure the trim symbol exists in the defs section
- ensure_symbol(document, 'trim')
+ if self.options.import_commands == "symbols":
+ # In most cases trims are inserted with the imported letters.
+ # Let's make sure the trim symbol exists in the defs section
+ ensure_symbol(document, 'trim')
- self.insert_baseline(document)
+ self.insert_baseline()
def get_glyph_element(self, glyph):
stitch_plan = generate_stitch_plan(str(glyph), self.options.import_commands)
@@ -77,5 +83,5 @@ class LettersToFont(InkstitchExtension):
stitch_plan.attrib.pop(INKSCAPE_GROUPMODE)
return stitch_plan
- def insert_baseline(self, document):
- document.namedview.add_guide(position=0.0, name="baseline")
+ def insert_baseline(self):
+ self.svg.namedview.add_guide(position=0.0, name="baseline")
diff --git a/lib/extensions/object_commands_toggle_visibility.py b/lib/extensions/object_commands_toggle_visibility.py
index 569f4305..e5d247e6 100644
--- a/lib/extensions/object_commands_toggle_visibility.py
+++ b/lib/extensions/object_commands_toggle_visibility.py
@@ -19,6 +19,6 @@ class ObjectCommandsToggleVisibility(InkstitchExtension):
for command_group in command_groups:
if first_iteration:
first_iteration = False
- if not command_group.is_visible():
+ if command_group.style('display', 'inline') == 'none':
display = "inline"
command_group.style['display'] = display
diff --git a/lib/extensions/params.py b/lib/extensions/params.py
index 540cc7bb..1ba144b2 100644
--- a/lib/extensions/params.py
+++ b/lib/extensions/params.py
@@ -7,7 +7,6 @@
import os
import sys
-import traceback
from collections import defaultdict
from copy import copy
from itertools import groupby, zip_longest
@@ -20,6 +19,7 @@ from ..commands import is_command, is_command_symbol
from ..elements import (Clone, EmbroideryElement, FillStitch, Polyline,
SatinColumn, Stroke)
from ..elements.clone import is_clone
+from ..exceptions import InkstitchException, format_uncaught_exception
from ..gui import PresetsPanel, SimulatorPreview, WarningPanel
from ..i18n import _
from ..svg.tags import SVG_POLYLINE_TAG
@@ -544,24 +544,22 @@ class SettingsFrame(wx.Frame):
patches.extend(copy(node).embroider(None))
check_stop_flag()
- except SystemExit:
- wx.CallAfter(self._show_warning)
+ except (SystemExit, ExitThread):
raise
- except ExitThread:
- raise
- except Exception as e:
- # Ignore errors. This can be things like incorrect paths for
- # satins or division by zero caused by incorrect param values.
- traceback.print_exception(e, file=sys.stderr)
- pass
+ except InkstitchException as exc:
+ wx.CallAfter(self._show_warning, str(exc))
+ except Exception:
+ wx.CallAfter(self._show_warning, format_uncaught_exception())
return patches
def _hide_warning(self):
+ self.warning_panel.clear()
self.warning_panel.Hide()
self.Layout()
- def _show_warning(self):
+ def _show_warning(self, warning_text):
+ self.warning_panel.set_warning_text(warning_text)
self.warning_panel.Show()
self.Layout()
diff --git a/lib/extensions/preferences.py b/lib/extensions/preferences.py
index 44c1b5aa..b78537c8 100644
--- a/lib/extensions/preferences.py
+++ b/lib/extensions/preferences.py
@@ -4,8 +4,7 @@
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
from .base import InkstitchExtension
-from ..api import APIServer
-from ..gui import open_url
+from ..gui.preferences import PreferencesApp
class Preferences(InkstitchExtension):
@@ -13,25 +12,6 @@ class Preferences(InkstitchExtension):
This saves embroider settings into the metadata of the file
'''
- def __init__(self, *args, **kwargs):
- InkstitchExtension.__init__(self, *args, **kwargs)
- self.arg_parser.add_argument("-c", "--collapse_len_mm",
- action="store", type=float,
- dest="collapse_length_mm", default=3.0,
- help="max collapse length (mm)")
- self.arg_parser.add_argument("-l", "--min_stitch_len_mm",
- action="store", type=float,
- dest="min_stitch_len_mm", default=0,
- help="minimum stitch length (mm)")
-
def effect(self):
- api_server = APIServer(self)
- port = api_server.start_server()
- electron = open_url("/preferences", port)
- electron.wait()
- api_server.stop()
- api_server.join()
-
- # self.metadata = self.get_inkstitch_metadata()
- # self.metadata['collapse_len_mm'] = self.options.collapse_length_mm
- # self.metadata['min_stitch_len_mm'] = self.options.min_stitch_len_mm
+ app = PreferencesApp(self)
+ app.MainLoop()
diff --git a/lib/extensions/remove_embroidery_settings.py b/lib/extensions/remove_embroidery_settings.py
index d8e6cb0e..b90d590b 100644
--- a/lib/extensions/remove_embroidery_settings.py
+++ b/lib/extensions/remove_embroidery_settings.py
@@ -5,7 +5,7 @@
from inkex import NSS, Boolean, ShapeElement
-from ..commands import find_commands
+from ..commands import OBJECT_COMMANDS, find_commands
from ..svg.svg import find_elements
from .base import InkstitchExtension
@@ -14,7 +14,7 @@ class RemoveEmbroiderySettings(InkstitchExtension):
def __init__(self, *args, **kwargs):
InkstitchExtension.__init__(self, *args, **kwargs)
self.arg_parser.add_argument("-p", "--del_params", dest="del_params", type=Boolean, default=True)
- self.arg_parser.add_argument("-c", "--del_commands", dest="del_commands", type=Boolean, default=False)
+ self.arg_parser.add_argument("-c", "--del_commands", dest="del_commands", type=str, default="none")
self.arg_parser.add_argument("-d", "--del_print", dest="del_print", type=Boolean, default=False)
def effect(self):
@@ -22,7 +22,7 @@ class RemoveEmbroiderySettings(InkstitchExtension):
if self.options.del_params:
self.remove_params()
- if self.options.del_commands:
+ if self.options.del_commands != 'none':
self.remove_commands()
if self.options.del_print:
self.remove_print_settings()
@@ -43,28 +43,53 @@ class RemoveEmbroiderySettings(InkstitchExtension):
elements = self.get_selected_elements()
self.remove_inkstitch_attributes(elements)
- def remove_commands(self):
- if not self.svg.selection:
- # remove intact command groups
- xpath = ".//svg:g[starts-with(@id,'command_group')]"
- groups = find_elements(self.svg, xpath)
- for group in groups:
+ def remove_all_commands(self):
+ xpath = ".//svg:g[starts-with(@id,'command_group')]"
+ groups = find_elements(self.svg, xpath)
+ for group in groups:
+ group.getparent().remove(group)
+
+ # remove standalone commands and ungrouped object commands
+ standalone_commands = ".//svg:use[starts-with(@xlink:href, '#inkstitch_')]|.//svg:path[starts-with(@id, 'command_connector')]"
+ self.remove_elements(standalone_commands)
+
+ # let's remove the symbols (defs), we won't need them in the document
+ symbols = ".//*[starts-with(@id, 'inkstitch_')]"
+ self.remove_elements(symbols)
+
+ def remove_specific_commands(self, command):
+ # remove object commands
+ if command in OBJECT_COMMANDS:
+ xlink = f"#inkstitch_{command}"
+ xpath = f".//svg:use[starts-with(@xlink:href, '{xlink}')]"
+ connectors = find_elements(self.svg, xpath)
+ for connector in connectors:
+ group = connector.getparent()
group.getparent().remove(group)
- else:
- elements = self.get_selected_elements()
- for element in elements:
- for command in find_commands(element):
+
+ # remove standalone commands and ungrouped object commands
+ standalone_commands = ".//svg:use[starts-with(@xlink:href, '#inkstitch_{command}')]"
+ self.remove_elements(standalone_commands)
+
+ # let's remove the symbols (defs), we won't need them in the document
+ symbols = f".//*[starts-with(@id, 'inkstitch_{command}')]"
+ self.remove_elements(symbols)
+
+ def remove_selected_commands(self):
+ elements = self.get_selected_elements()
+ for element in elements:
+ for command in find_commands(element):
+ if self.options.del_commands in ('all', command.command):
group = command.connector.getparent()
group.getparent().remove(group)
- if not self.svg.selection:
- # remove standalone commands and ungrouped object commands
- standalone_commands = ".//svg:use[starts-with(@xlink:href, '#inkstitch_')]|.//svg:path[starts-with(@id, 'command_connector')]"
- self.remove_elements(standalone_commands)
-
- # let's remove the symbols (defs), we won't need them in the document
- symbols = ".//*[starts-with(@id, 'inkstitch_')]"
- self.remove_elements(symbols)
+ def remove_commands(self):
+ if self.svg.selection:
+ self.remove_selected_commands()
+ elif self.options.del_commands == "all":
+ self.remove_all_commands()
+ else:
+ self.remove_specific_commands(self.options.del_commands)
def get_selected_elements(self):
return self.svg.selection.get(ShapeElement)
diff --git a/lib/extensions/stroke_to_lpe_satin.py b/lib/extensions/stroke_to_lpe_satin.py
index b89e471c..96cab4e9 100644
--- a/lib/extensions/stroke_to_lpe_satin.py
+++ b/lib/extensions/stroke_to_lpe_satin.py
@@ -209,7 +209,7 @@ class SatinPattern:
return str(path1) + str(path2) + rungs
-satin_patterns = {'normal': SatinPattern('M 0,0.4 H 4 H 8', 'cc'),
+satin_patterns = {'normal': SatinPattern('M 0,0 H 4 H 8', 'cc'),
'pearl': SatinPattern('M 0,0 C 0,0.22 0.18,0.4 0.4,0.4 0.62,0.4 0.8,0.22 0.8,0', 'csc'),
'diamond': SatinPattern('M 0,0 0.4,0.2 0.8,0', 'ccc'),
'triangle': SatinPattern('M 0,0 0.4,0.1 0.78,0.2 0.8,0', 'cccc'),
diff --git a/lib/extensions/zigzag_line_to_satin.py b/lib/extensions/zigzag_line_to_satin.py
index 167f4b91..b71bf6a0 100644
--- a/lib/extensions/zigzag_line_to_satin.py
+++ b/lib/extensions/zigzag_line_to_satin.py
@@ -23,11 +23,12 @@ class ZigzagLineToSatin(InkstitchExtension):
self.arg_parser.add_argument("-l", "--reduce-rungs", type=inkex.Boolean, default=False, dest="reduce_rungs")
def effect(self):
- if not self.svg.selection or not self.get_elements():
+ nodes = self.get_selection(self.svg.selection)
+ if not nodes:
inkex.errormsg(_("Please select at least one stroke to convert to a satin column."))
return
- for node in self.svg.selection:
+ for node in nodes:
d = []
point_list = list(node.get_path().end_points)
# find duplicated nodes (= do not smooth)
@@ -49,6 +50,17 @@ class ZigzagLineToSatin(InkstitchExtension):
node.set('d', " ".join(d))
node.set('inkstitch:satin_column', True)
+ def get_selection(self, nodes):
+ selection = []
+ for node in nodes:
+ # we only apply to path elements, no use in converting ellipses or rectangles, etc.
+ if node.TAG == "path":
+ selection.append(node)
+ elif node.TAG == "g":
+ for element in node.descendants():
+ selection.extend(self.get_selection(element))
+ return selection
+
def _get_sharp_edge_nodes(self, point_list):
points = []
sharp_edges = []
diff --git a/lib/extensions/zip.py b/lib/extensions/zip.py
index e80bc34c..b3183a9a 100644
--- a/lib/extensions/zip.py
+++ b/lib/extensions/zip.py
@@ -9,15 +9,17 @@ import tempfile
from copy import deepcopy
from zipfile import ZipFile
+from inkex import Boolean
from lxml import etree
import pyembroidery
-from inkex import Boolean
from ..i18n import _
from ..output import write_embroidery_file
from ..stitch_plan import stitch_groups_to_stitch_plan
+from ..svg import PIXELS_PER_MM
from ..threads import ThreadCatalog
+from ..utils.geometry import Point
from .base import InkstitchExtension
@@ -25,6 +27,11 @@ class Zip(InkstitchExtension):
def __init__(self, *args, **kwargs):
InkstitchExtension.__init__(self)
+ self.arg_parser.add_argument('--notebook', type=Boolean, default=True)
+ self.arg_parser.add_argument('--file-formats', type=Boolean, default=True)
+ self.arg_parser.add_argument('--panelization', type=Boolean, default=True)
+ self.arg_parser.add_argument('--output-options', type=Boolean, default=True)
+
# it's kind of obnoxious that I have to do this...
self.formats = []
for format in pyembroidery.supported_formats():
@@ -33,10 +40,17 @@ class Zip(InkstitchExtension):
self.arg_parser.add_argument('--format-%s' % extension, type=Boolean, dest=extension)
self.formats.append(extension)
self.arg_parser.add_argument('--format-svg', type=Boolean, dest='svg')
- self.arg_parser.add_argument('--format-threadlist', type=Boolean, dest='threadlist')
self.formats.append('svg')
+ self.arg_parser.add_argument('--format-threadlist', type=Boolean, dest='threadlist')
self.formats.append('threadlist')
+ self.arg_parser.add_argument('--x-repeats', type=int, dest='x_repeats', default=1)
+ self.arg_parser.add_argument('--y-repeats', type=int, dest='y_repeats', default=1)
+ self.arg_parser.add_argument('--x-spacing', type=float, dest='x_spacing', default=100)
+ self.arg_parser.add_argument('--y-spacing', type=float, dest='y_spacing', default=100)
+
+ self.arg_parser.add_argument('--custom-file-name', type=str, dest='custom_file_name', default='')
+
def effect(self):
if not self.get_elements():
return
@@ -47,7 +61,10 @@ class Zip(InkstitchExtension):
patches = self.elements_to_stitch_groups(self.elements)
stitch_plan = stitch_groups_to_stitch_plan(patches, collapse_len=collapse_len, min_stitch_len=min_stitch_len)
- base_file_name = self.get_base_file_name()
+ if self.options.x_repeats != 1 or self.options.y_repeats != 1:
+ stitch_plan = self._make_offsets(stitch_plan)
+
+ base_file_name = self._get_file_name()
path = tempfile.mkdtemp()
files = []
@@ -93,6 +110,22 @@ class Zip(InkstitchExtension):
# don't let inkex output the SVG!
sys.exit(0)
+ def _get_file_name(self):
+ if self.options.custom_file_name:
+ base_file_name = self.options.custom_file_name
+ else:
+ base_file_name = self.get_base_file_name()
+ return base_file_name
+
+ def _make_offsets(self, stitch_plan):
+ dx = self.options.x_spacing * PIXELS_PER_MM
+ dy = self.options.y_spacing * PIXELS_PER_MM
+ offsets = []
+ for x in range(self.options.x_repeats):
+ for y in range(self.options.y_repeats):
+ offsets.append(Point(x * dx, y * dy))
+ return stitch_plan.make_offsets(offsets)
+
def get_threadlist(self, stitch_plan, design_name):
ThreadCatalog().match_and_apply_palette(stitch_plan, self.get_inkstitch_metadata()['thread-palette'])
thread_used = []