summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/api/preferences.py41
-rw-r--r--lib/api/server.py2
-rw-r--r--lib/api/stitch_plan.py20
-rw-r--r--lib/commands.py2
-rw-r--r--lib/elements/element.py55
-rw-r--r--lib/elements/fill_stitch.py70
-rw-r--r--lib/elements/satin_column.py54
-rw-r--r--lib/exceptions.py30
-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
-rw-r--r--lib/gui/preferences.py217
-rw-r--r--lib/gui/preferences.wxg305
-rw-r--r--lib/gui/simulator.py459
-rw-r--r--lib/gui/warnings.py21
-rw-r--r--lib/stitch_plan/color_block.py23
-rw-r--r--lib/stitch_plan/stitch.py10
-rw-r--r--lib/stitch_plan/stitch_plan.py9
-rw-r--r--lib/stitches/meander_fill.py7
-rw-r--r--lib/svg/path.py15
-rw-r--r--lib/threads/color.py19
29 files changed, 1304 insertions, 328 deletions
diff --git a/lib/api/preferences.py b/lib/api/preferences.py
deleted file mode 100644
index bc8328b8..00000000
--- a/lib/api/preferences.py
+++ /dev/null
@@ -1,41 +0,0 @@
-# Authors: see git history
-#
-# Copyright (c) 2010 Authors
-# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
-
-from flask import Blueprint, g, jsonify, request
-
-from ..utils.cache import get_stitch_plan_cache
-from ..utils.settings import global_settings
-
-preferences = Blueprint('preferences', __name__)
-
-
-@preferences.route('/', methods=["POST"])
-def update_preferences():
- metadata = g.extension.get_inkstitch_metadata()
- metadata.update(request.json['this_svg_settings'])
- global_settings.update(request.json['global_settings'])
-
- # cache size may have changed
- stitch_plan_cache = get_stitch_plan_cache()
- stitch_plan_cache.size_limit = global_settings['cache_size'] * 1024 * 1024
- stitch_plan_cache.cull()
-
- return jsonify({"status": "success"})
-
-
-@preferences.route('/', methods=["GET"])
-def get_preferences():
- metadata = g.extension.get_inkstitch_metadata()
- return jsonify({"status": "success",
- "this_svg_settings": metadata,
- "global_settings": global_settings
- })
-
-
-@preferences.route('/clear_cache', methods=["POST"])
-def clear_cache():
- stitch_plan_cache = get_stitch_plan_cache()
- stitch_plan_cache.clear(retry=True)
- return jsonify({"status": "success"})
diff --git a/lib/api/server.py b/lib/api/server.py
index 26efa521..5625d77d 100644
--- a/lib/api/server.py
+++ b/lib/api/server.py
@@ -18,7 +18,6 @@ from werkzeug.serving import make_server
from ..utils.json import InkStitchJSONProvider
from .simulator import simulator
from .stitch_plan import stitch_plan
-from .preferences import preferences
from .page_specs import page_specs
from .lang import languages
# this for electron axios
@@ -50,7 +49,6 @@ class APIServer(Thread):
self.app.register_blueprint(simulator, url_prefix="/simulator")
self.app.register_blueprint(stitch_plan, url_prefix="/stitch_plan")
- self.app.register_blueprint(preferences, url_prefix="/preferences")
self.app.register_blueprint(page_specs, url_prefix="/page_specs")
self.app.register_blueprint(languages, url_prefix="/languages")
diff --git a/lib/api/stitch_plan.py b/lib/api/stitch_plan.py
index c70efd98..5e9a57c1 100644
--- a/lib/api/stitch_plan.py
+++ b/lib/api/stitch_plan.py
@@ -5,9 +5,9 @@
from flask import Blueprint, g, jsonify
+from ..exceptions import InkstitchException, format_uncaught_exception
from ..stitch_plan import stitch_groups_to_stitch_plan
-
stitch_plan = Blueprint('stitch_plan', __name__)
@@ -16,10 +16,14 @@ def get_stitch_plan():
if not g.extension.get_elements():
return dict(colors=[], stitch_blocks=[], commands=[])
- metadata = g.extension.get_inkstitch_metadata()
- collapse_len = metadata['collapse_len_mm']
- min_stitch_len = metadata['min_stitch_len_mm']
- patches = g.extension.elements_to_stitch_groups(g.extension.elements)
- stitch_plan = stitch_groups_to_stitch_plan(patches, collapse_len=collapse_len, min_stitch_len=min_stitch_len)
-
- return jsonify(stitch_plan)
+ try:
+ metadata = g.extension.get_inkstitch_metadata()
+ collapse_len = metadata['collapse_len_mm']
+ min_stitch_len = metadata['min_stitch_len_mm']
+ patches = g.extension.elements_to_stitch_groups(g.extension.elements)
+ stitch_plan = stitch_groups_to_stitch_plan(patches, collapse_len=collapse_len, min_stitch_len=min_stitch_len)
+ return jsonify(stitch_plan)
+ except InkstitchException as exc:
+ return jsonify({"error_message": str(exc)}), 500
+ except Exception:
+ return jsonify({"error_message": format_uncaught_exception()}), 500
diff --git a/lib/commands.py b/lib/commands.py
index d93954ec..8c43aed3 100644
--- a/lib/commands.py
+++ b/lib/commands.py
@@ -124,6 +124,8 @@ class Command(BaseCommand):
def parse_command(self):
path = self.parse_connector_path()
+ if len(path) == 0:
+ raise CommandParseError("connector has no path information")
neighbors = [
(self.get_node_by_url(self.connector.get(CONNECTION_START)), path[0][0][1]),
diff --git a/lib/elements/element.py b/lib/elements/element.py
index 43cbc8a2..963653af 100644
--- a/lib/elements/element.py
+++ b/lib/elements/element.py
@@ -3,6 +3,7 @@
# Copyright (c) 2010 Authors
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
import sys
+from contextlib import contextmanager
from copy import deepcopy
import inkex
@@ -11,6 +12,7 @@ from inkex import bezier
from ..commands import find_commands
from ..debug import debug
+from ..exceptions import InkstitchException, format_uncaught_exception
from ..i18n import _
from ..marker import get_marker_elements_cache_key_data
from ..patterns import apply_patterns, get_patterns_cache_key_data
@@ -546,23 +548,25 @@ class EmbroideryElement(object):
def embroider(self, last_stitch_group):
debug.log(f"starting {self.node.get('id')} {self.node.get(INKSCAPE_LABEL)}")
- if last_stitch_group:
- previous_stitch = last_stitch_group.stitches[-1]
- else:
- previous_stitch = None
- stitch_groups = self._load_cached_stitch_groups(previous_stitch)
- if not stitch_groups:
- self.validate()
+ with self.handle_unexpected_exceptions():
+ if last_stitch_group:
+ previous_stitch = last_stitch_group.stitches[-1]
+ else:
+ previous_stitch = None
+ stitch_groups = self._load_cached_stitch_groups(previous_stitch)
+
+ if not stitch_groups:
+ self.validate()
- stitch_groups = self.to_stitch_groups(last_stitch_group)
- apply_patterns(stitch_groups, self.node)
+ stitch_groups = self.to_stitch_groups(last_stitch_group)
+ apply_patterns(stitch_groups, self.node)
- if stitch_groups:
- stitch_groups[-1].trim_after = self.has_command("trim") or self.trim_after
- stitch_groups[-1].stop_after = self.has_command("stop") or self.stop_after
+ if stitch_groups:
+ stitch_groups[-1].trim_after = self.has_command("trim") or self.trim_after
+ stitch_groups[-1].stop_after = self.has_command("stop") or self.stop_after
- self._save_cached_stitch_groups(stitch_groups, previous_stitch)
+ self._save_cached_stitch_groups(stitch_groups, previous_stitch)
debug.log(f"ending {self.node.get('id')} {self.node.get(INKSCAPE_LABEL)}")
return stitch_groups
@@ -575,14 +579,27 @@ class EmbroideryElement(object):
else:
name = id
- # L10N used when showing an error message to the user such as
- # "Failed on PathLabel (path1234): Satin column: One or more of the rungs doesn't intersect both rails."
- error_msg = "%s %s: %s" % (_("Failed on "), name, message)
+ error_msg = f"{name}: {message}"
if point_to_troubleshoot:
error_msg += "\n\n%s" % _("Please run Extensions > Ink/Stitch > Troubleshoot > Troubleshoot objects. "
- "This will indicate the errorneus position.")
- inkex.errormsg(error_msg)
- sys.exit(1)
+ "This will show you the exact location of the problem.")
+
+ raise InkstitchException(error_msg)
+
+ @contextmanager
+ def handle_unexpected_exceptions(self):
+ try:
+ # This runs the code in the `with` body so that we can catch
+ # exceptions.
+ yield
+ except (InkstitchException, SystemExit, KeyboardInterrupt):
+ raise
+ except Exception:
+ if hasattr(sys, 'gettrace') and sys.gettrace():
+ # if we're debugging, let the exception bubble up
+ raise
+
+ raise InkstitchException(format_uncaught_exception())
def validation_errors(self):
"""Return a list of errors with this Element.
diff --git a/lib/elements/fill_stitch.py b/lib/elements/fill_stitch.py
index b93d7ff5..c7c3c640 100644
--- a/lib/elements/fill_stitch.py
+++ b/lib/elements/fill_stitch.py
@@ -6,8 +6,6 @@
import logging
import math
import re
-import sys
-import traceback
import numpy as np
from inkex import Transform
@@ -25,9 +23,8 @@ from ..stitches.meander_fill import meander_fill
from ..svg import PIXELS_PER_MM, get_node_transform
from ..svg.clip import get_clip_path
from ..svg.tags import INKSCAPE_LABEL
-from ..utils import cache, version
+from ..utils import cache
from ..utils.param import ParamOption
-from ..utils.threading import ExitThread
from .element import EmbroideryElement, param
from .validation import ValidationError, ValidationWarning
@@ -706,30 +703,25 @@ class FillStitch(EmbroideryElement):
for shape in self.shape.geoms:
start = self.get_starting_point(previous_stitch_group)
- try:
- if self.fill_underlay:
- underlay_shapes = self.underlay_shape(shape)
- for underlay_shape in underlay_shapes.geoms:
- underlay_stitch_groups, start = self.do_underlay(underlay_shape, start)
- stitch_groups.extend(underlay_stitch_groups)
-
- fill_shapes = self.fill_shape(shape)
- for i, fill_shape in enumerate(fill_shapes.geoms):
- if self.fill_method == 'contour_fill':
- stitch_groups.extend(self.do_contour_fill(fill_shape, previous_stitch_group, start))
- elif self.fill_method == 'guided_fill':
- stitch_groups.extend(self.do_guided_fill(fill_shape, previous_stitch_group, start, end))
- elif self.fill_method == 'meander_fill':
- stitch_groups.extend(self.do_meander_fill(fill_shape, shape, i, start, end))
- elif self.fill_method == 'circular_fill':
- stitch_groups.extend(self.do_circular_fill(fill_shape, previous_stitch_group, start, end))
- else:
- # auto_fill
- stitch_groups.extend(self.do_auto_fill(fill_shape, previous_stitch_group, start, end))
- except ExitThread:
- raise
- except Exception:
- self.fatal_fill_error()
+ if self.fill_underlay:
+ underlay_shapes = self.underlay_shape(shape)
+ for underlay_shape in underlay_shapes.geoms:
+ underlay_stitch_groups, start = self.do_underlay(underlay_shape, start)
+ stitch_groups.extend(underlay_stitch_groups)
+
+ fill_shapes = self.fill_shape(shape)
+ for i, fill_shape in enumerate(fill_shapes.geoms):
+ if self.fill_method == 'contour_fill':
+ stitch_groups.extend(self.do_contour_fill(fill_shape, previous_stitch_group, start))
+ elif self.fill_method == 'guided_fill':
+ stitch_groups.extend(self.do_guided_fill(fill_shape, previous_stitch_group, start, end))
+ elif self.fill_method == 'meander_fill':
+ stitch_groups.extend(self.do_meander_fill(fill_shape, shape, i, start, end))
+ elif self.fill_method == 'circular_fill':
+ stitch_groups.extend(self.do_circular_fill(fill_shape, previous_stitch_group, start, end))
+ else:
+ # auto_fill
+ stitch_groups.extend(self.do_auto_fill(fill_shape, previous_stitch_group, start, end))
previous_stitch_group = stitch_groups[-1]
return stitch_groups
@@ -885,28 +877,6 @@ class FillStitch(EmbroideryElement):
else:
return guide_lines['stroke'][0]
- def fatal_fill_error(self):
- if hasattr(sys, 'gettrace') and sys.gettrace():
- # if we're debugging, let the exception bubble up
- raise
-
- # for an uncaught exception, give a little more info so that they can create a bug report
- message = ""
- message += _("Error during autofill! This means it is a bug in Ink/Stitch.")
- message += "\n\n"
- # L10N this message is followed by a URL: https://github.com/inkstitch/inkstitch/issues/new
- message += _("If you'd like to help please\n"
- "- copy the entire error message below\n"
- "- save your SVG file and\n"
- "- create a new issue at")
- message += " https://github.com/inkstitch/inkstitch/issues/new\n\n"
- message += _("Include the error description and also (if possible) the svg file.")
- message += '\n\n\n'
- message += version.get_inkstitch_version() + '\n'
- message += traceback.format_exc()
-
- self.fatal(message)
-
def do_circular_fill(self, shape, last_patch, starting_point, ending_point):
# get target position
command = self.get_command('ripple_target')
diff --git a/lib/elements/satin_column.py b/lib/elements/satin_column.py
index 25f50e13..a63ca403 100644
--- a/lib/elements/satin_column.py
+++ b/lib/elements/satin_column.py
@@ -280,7 +280,7 @@ class SatinColumn(EmbroideryElement):
def reverse_rails(self):
return self.get_param('reverse_rails', 'automatic')
- def _get_rails_to_reverse(self, rails):
+ def _get_rails_to_reverse(self):
choice = self.reverse_rails
if choice == 'first':
@@ -290,6 +290,7 @@ class SatinColumn(EmbroideryElement):
elif choice == 'both':
return True, True
elif choice == 'automatic':
+ rails = [shgeo.LineString(self.flatten_subpath(rail)) for rail in self.rails]
if len(rails) == 2:
# Sample ten points along the rails. Compare the distance
# between corresponding points on both rails with and without
@@ -314,7 +315,7 @@ class SatinColumn(EmbroideryElement):
# reverse the second rail
return False, True
- return None
+ return False, False
@property
@param(
@@ -508,7 +509,7 @@ class SatinColumn(EmbroideryElement):
"""The rails, as LineStrings."""
paths = [shgeo.LineString(self.flatten_subpath(rail)) for rail in self.rails]
- rails_to_reverse = self._get_rails_to_reverse(paths)
+ rails_to_reverse = self._get_rails_to_reverse()
if paths and rails_to_reverse is not None:
for i, reverse in enumerate(rails_to_reverse):
if reverse:
@@ -537,14 +538,19 @@ class SatinColumn(EmbroideryElement):
else:
return [subpath for i, subpath in enumerate(self.csp) if i not in self.rail_indices]
+ @cache
def _synthesize_rungs(self):
rung_endpoints = []
# check for unequal length of rails
equal_length = len(self.rails[0]) == len(self.rails[1])
- for rail in self.rails:
+ rails_to_reverse = self._get_rails_to_reverse()
+ for i, rail in enumerate(self.rails):
points = self.strip_control_points(rail)
+ if rails_to_reverse[i]:
+ points = points[::-1]
+
if len(points) > 2 or not equal_length:
# Don't bother putting rungs at the start and end.
points = points[1:-1]
@@ -744,6 +750,9 @@ class SatinColumn(EmbroideryElement):
Returns two new SatinColumn instances: the part before and the part
after the split point. All parameters are copied over to the new
SatinColumn instances.
+
+ The returned SatinColumns will not be in the SVG document and will have
+ their transforms applied.
"""
cut_points = self._find_cut_points(split_point)
@@ -856,6 +865,43 @@ class SatinColumn(EmbroideryElement):
return SatinColumn(node)
+ def merge(self, satin):
+ """Merge this satin with another satin
+
+ This method expects that the provided satin continues on directly after
+ this one, as would be the case, for example, if the two satins were the
+ result of the split() method.
+
+ Returns a new SatinColumn instance that combines the rails and rungs of
+ this satin and the provided satin. A rung is added at the end of this
+ satin.
+
+ The returned SatinColumn will not be in the SVG document and will have
+ its transforms applied.
+ """
+ rails = [self.flatten_subpath(rail) for rail in self.rails]
+ other_rails = [satin.flatten_subpath(rail) for rail in satin.rails]
+
+ if len(rails) != 2 or len(other_rails) != 2:
+ # weird non-satin things, give up and don't merge
+ return self
+
+ # remove first node of each other rail before merging (avoid duplicated nodes)
+ rails[0].extend(other_rails[0][1:])
+ rails[1].extend(other_rails[1][1:])
+
+ rungs = [self.flatten_subpath(rung) for rung in self.rungs]
+ other_rungs = [satin.flatten_subpath(rung) for rung in satin.rungs]
+
+ # add a rung in between the two satins and extend it just a litte to ensure it is crossing the rails
+ new_rung = shgeo.LineString([other_rails[0][0], other_rails[1][0]])
+ rungs.append(list(shaffinity.scale(new_rung, 1.2, 1.2).coords))
+
+ # add on the other satin's rungs
+ rungs.extend(other_rungs)
+
+ return self._csp_to_satin(point_lists_to_csp(rails + rungs))
+
@property
@cache
def center_line(self):
diff --git a/lib/exceptions.py b/lib/exceptions.py
index a9820ac3..3a6b456c 100644
--- a/lib/exceptions.py
+++ b/lib/exceptions.py
@@ -2,6 +2,36 @@
#
# Copyright (c) 2010 Authors
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
+import traceback
+
class InkstitchException(Exception):
pass
+
+
+def format_uncaught_exception():
+ """Format the current exception as a request for a bug report.
+
+ Call this inside an except block so that there is an exception that we can
+ call traceback.format_exc() on.
+ """
+
+ # importing locally to avoid any possibility of circular import
+ from lib.utils import version
+ from .i18n import _
+
+ message = ""
+ message += _("Ink/Stitch experienced an unexpected error. This means it is a bug in Ink/Stitch.")
+ message += "\n\n"
+ # L10N this message is followed by a URL: https://github.com/inkstitch/inkstitch/issues/new
+ message += _("If you'd like to help please\n"
+ "- copy the entire error message below\n"
+ "- save your SVG file and\n"
+ "- create a new issue at")
+ message += " https://github.com/inkstitch/inkstitch/issues/new\n\n"
+ message += _("Include the error description and also (if possible) the svg file.")
+ message += '\n\n\n'
+ message += version.get_inkstitch_version() + '\n'
+ message += traceback.format_exc()
+
+ return message
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 = []
diff --git a/lib/gui/preferences.py b/lib/gui/preferences.py
new file mode 100644
index 00000000..14c0d4dd
--- /dev/null
+++ b/lib/gui/preferences.py
@@ -0,0 +1,217 @@
+# 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 wx
+
+from ..i18n import _
+from ..utils.cache import get_stitch_plan_cache
+from ..utils.settings import global_settings
+
+
+class PreferencesFrame(wx.Frame):
+ def __init__(self, *args, **kwargs):
+ self.extension = kwargs.pop("extension")
+ wx.Frame.__init__(self, None, wx.ID_ANY, _("Preferences"), *args, **kwargs)
+ self.SetTitle(_("Preferences"))
+
+ metadata = self.extension.get_inkstitch_metadata()
+
+ self.panel_1 = wx.Panel(self, wx.ID_ANY)
+
+ main_sizer = wx.BoxSizer(wx.VERTICAL)
+
+ self.notebook = wx.Notebook(self.panel_1, wx.ID_ANY)
+ main_sizer.Add(self.notebook, 1, wx.ALL | wx.EXPAND, 10)
+
+ self.this_svg_page = wx.Panel(self.notebook, wx.ID_ANY)
+ self.notebook.AddPage(self.this_svg_page, _("This SVG"))
+
+ sizer_1 = wx.BoxSizer(wx.VERTICAL)
+
+ # add space above and below to center sizer_2 vertically
+ sizer_1.Add((0, 20), 1, wx.EXPAND, 0)
+
+ sizer_2 = wx.FlexGridSizer(2, 4, 15, 10)
+ sizer_1.Add(sizer_2, 0, wx.EXPAND | wx.LEFT | wx.RIGHT, 20)
+
+ label_1 = wx.StaticText(self.this_svg_page, wx.ID_ANY, _("Minimum jump stitch length"), style=wx.ALIGN_LEFT)
+ label_1.SetToolTip(_("Jump stitches smaller than this will be treated as normal stitches."))
+ sizer_2.Add(label_1, 1, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 15)
+
+ self.minimum_jump_stitch_length = wx.SpinCtrlDouble(
+ self.this_svg_page, wx.ID_ANY, inc=0.1,
+ value=str(metadata['collapse_len_mm'] or global_settings['default_collapse_len_mm']),
+ style=wx.ALIGN_RIGHT | wx.SP_ARROW_KEYS
+ )
+ self.minimum_jump_stitch_length.SetDigits(1)
+ sizer_2.Add(self.minimum_jump_stitch_length, 0, wx.ALIGN_CENTER_VERTICAL, 0)
+
+ label_2 = wx.StaticText(self.this_svg_page, wx.ID_ANY, _("mm"))
+ sizer_2.Add(label_2, 0, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 15)
+
+ self.button_1 = wx.Button(self.this_svg_page, wx.ID_ANY, _("Set As Default"))
+ sizer_2.Add(self.button_1, 0, wx.ALIGN_CENTER_VERTICAL, 0)
+
+ label_3 = wx.StaticText(self.this_svg_page, wx.ID_ANY, _("Minimum stitch length"))
+ sizer_2.Add(label_3, 0, 0, 0)
+
+ self.minimum_stitch_length = wx.SpinCtrlDouble(
+ self.this_svg_page, wx.ID_ANY, inc=0.1,
+ value=str(metadata['min_stitch_len_mm'] or global_settings['default_min_stitch_len_mm']),
+ style=wx.ALIGN_RIGHT | wx.SP_ARROW_KEYS
+ )
+ self.minimum_stitch_length.SetDigits(1)
+ sizer_2.Add(self.minimum_stitch_length, 0, wx.ALIGN_CENTER_VERTICAL, 0)
+
+ label_4 = wx.StaticText(self.this_svg_page, wx.ID_ANY, _("mm"))
+ sizer_2.Add(label_4, 0, wx.ALIGN_CENTER_VERTICAL, 0)
+
+ self.button_2 = wx.Button(self.this_svg_page, wx.ID_ANY, _("Set As Default"))
+ sizer_2.Add(self.button_2, 0, wx.ALIGN_CENTER_VERTICAL, 0)
+
+ sizer_1.Add((0, 20), 1, wx.EXPAND, 0)
+
+ self.global_page = wx.Panel(self.notebook, wx.ID_ANY)
+ self.notebook.AddPage(self.global_page, _("Global"))
+
+ sizer_3 = wx.BoxSizer(wx.VERTICAL)
+
+ # add space above and below to center sizer_4 vertically
+ sizer_3.Add((0, 20), 1, wx.EXPAND, 0)
+
+ sizer_4 = wx.FlexGridSizer(3, 4, 15, 10)
+ sizer_3.Add(sizer_4, 0, wx.EXPAND | wx.LEFT | wx.RIGHT, 20)
+
+ label_5 = wx.StaticText(self.global_page, wx.ID_ANY, _("Default minimum jump stitch length"), style=wx.ALIGN_LEFT)
+ label_5.SetToolTip(_("Jump stitches smaller than this will be treated as normal stitches."))
+ sizer_4.Add(label_5, 1, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 15)
+
+ self.default_minimum_jump_stitch_length = wx.SpinCtrlDouble(
+ self.global_page, wx.ID_ANY, inc=0.1,
+ value=str(global_settings['default_collapse_len_mm']),
+ style=wx.ALIGN_RIGHT | wx.SP_ARROW_KEYS
+ )
+ self.default_minimum_jump_stitch_length.SetDigits(1)
+ sizer_4.Add(self.default_minimum_jump_stitch_length, 0, wx.ALIGN_CENTER_VERTICAL, 0)
+
+ label_6 = wx.StaticText(self.global_page, wx.ID_ANY, _("mm"))
+ sizer_4.Add(label_6, 0, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 15)
+
+ sizer_4.Add((0, 0), 0, 0, 0)
+
+ label_7 = wx.StaticText(self.global_page, wx.ID_ANY, _("Minimum stitch length"))
+ sizer_4.Add(label_7, 0, wx.ALIGN_CENTER_VERTICAL, 0)
+
+ self.default_minimum_stitch_length = wx.SpinCtrlDouble(
+ self.global_page, wx.ID_ANY, inc=0.1,
+ value=str(global_settings['default_min_stitch_len_mm']),
+ style=wx.ALIGN_RIGHT | wx.SP_ARROW_KEYS
+ )
+ self.default_minimum_stitch_length.SetDigits(1)
+ sizer_4.Add(self.default_minimum_stitch_length, 0, wx.ALIGN_CENTER_VERTICAL, 0)
+
+ label_8 = wx.StaticText(self.global_page, wx.ID_ANY, _("mm"))
+ sizer_4.Add(label_8, 0, wx.ALIGN_CENTER_VERTICAL, 0)
+
+ sizer_4.Add((0, 20), 0, 0, 0)
+
+ label_9 = wx.StaticText(self.global_page, wx.ID_ANY, _("Stitch plan cache size"), style=wx.ALIGN_LEFT)
+ label_9.SetToolTip(_("Jump stitches smaller than this will be treated as normal stitches."))
+ sizer_4.Add(label_9, 1, wx.ALIGN_CENTER_VERTICAL, 0)
+
+ self.stitch_plan_cache_size = wx.SpinCtrl(
+ self.global_page, wx.ID_ANY,
+ value=str(global_settings['cache_size']),
+ style=wx.ALIGN_RIGHT | wx.SP_ARROW_KEYS
+ )
+ self.stitch_plan_cache_size.SetIncrement(10)
+ sizer_4.Add(self.stitch_plan_cache_size, 0, wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT, 0)
+
+ label_10 = wx.StaticText(self.global_page, wx.ID_ANY, _("MB"))
+ sizer_4.Add(label_10, 0, wx.ALIGN_CENTER_VERTICAL, 0)
+
+ self.clear_cache_button = wx.Button(self.global_page, wx.ID_ANY, _("Clear Stitch Plan Cache"))
+ sizer_4.Add(self.clear_cache_button, 0, wx.ALIGN_CENTER_VERTICAL, 0)
+
+ sizer_3.Add((0, 0), 1, wx.EXPAND, 0)
+
+ button_sizer = wx.BoxSizer(wx.HORIZONTAL)
+ main_sizer.Add(button_sizer, 0, wx.BOTTOM | wx.EXPAND | wx.LEFT | wx.RIGHT, 10)
+
+ button_sizer.Add((0, 0), 1, 0, 0)
+
+ self.cancel_button = wx.Button(self.panel_1, wx.ID_CANCEL, "")
+ button_sizer.Add(self.cancel_button, 0, wx.RIGHT, 10)
+
+ self.ok_button = wx.Button(self.panel_1, wx.ID_OK, "")
+ button_sizer.Add(self.ok_button, 0, 0, 0)
+
+ sizer_4.AddGrowableCol(0)
+
+ self.global_page.SetSizer(sizer_3)
+
+ sizer_2.AddGrowableCol(0)
+
+ self.this_svg_page.SetSizer(sizer_1)
+
+ self.panel_1.SetSizer(main_sizer)
+
+ main_sizer.Fit(self)
+ self.Layout()
+ self.SetSizeHints(main_sizer.CalcMin())
+
+ self.Bind(wx.EVT_BUTTON, self.set_as_default_minimum_jump_stitch_length, self.button_1)
+ self.Bind(wx.EVT_BUTTON, self.set_as_default_minimum_stitch_length, self.button_2)
+ self.Bind(wx.EVT_BUTTON, self.clear_cache, self.clear_cache_button)
+ self.Bind(wx.EVT_BUTTON, self.cancel_button_clicked, self.cancel_button)
+ self.Bind(wx.EVT_BUTTON, self.ok_button_clicked, self.ok_button)
+
+ def set_as_default_minimum_jump_stitch_length(self, event):
+ self.default_minimum_jump_stitch_length.SetValue(self.minimum_jump_stitch_length.GetValue())
+
+ def set_as_default_minimum_stitch_length(self, event):
+ self.default_minimum_stitch_length.SetValue(self.minimum_stitch_length.GetValue())
+
+ def clear_cache(self, event):
+ stitch_plan_cache = get_stitch_plan_cache()
+ stitch_plan_cache.clear(retry=True)
+
+ def apply(self):
+ metadata = self.extension.get_inkstitch_metadata()
+ metadata['min_stitch_len_mm'] = self.minimum_stitch_length.GetValue()
+ metadata['collapse_len_mm'] = self.minimum_jump_stitch_length.GetValue()
+
+ global_settings['default_min_stitch_len_mm'] = self.default_minimum_stitch_length.GetValue()
+ global_settings['default_collapse_len_mm'] = self.default_minimum_jump_stitch_length.GetValue()
+ global_settings['cache_size'] = self.stitch_plan_cache_size.GetValue()
+
+ # cache size may have changed
+ stitch_plan_cache = get_stitch_plan_cache()
+ stitch_plan_cache.size_limit = int(global_settings['cache_size'] * 1024 * 1024)
+ stitch_plan_cache.cull()
+
+ def cancel_button_clicked(self, event):
+ self.Destroy()
+
+ def ok_button_clicked(self, event):
+ self.apply()
+ self.Destroy()
+
+
+class PreferencesApp(wx.App):
+ def __init__(self, extension):
+ self.extension = extension
+ super().__init__()
+
+ def OnInit(self):
+ self.frame = PreferencesFrame(extension=self.extension)
+ self.SetTopWindow(self.frame)
+ self.frame.Show()
+ return True
+
+
+if __name__ == "__main__":
+ app = PreferencesApp(None)
+ app.MainLoop()
diff --git a/lib/gui/preferences.wxg b/lib/gui/preferences.wxg
new file mode 100644
index 00000000..328ccce3
--- /dev/null
+++ b/lib/gui/preferences.wxg
@@ -0,0 +1,305 @@
+<?xml version="1.0"?>
+<!-- generated by wxGlade 1.0.5 on Tue Aug 15 00:11:05 2023 -->
+
+<application class="PreferencesApp" encoding="UTF-8" for_version="3.0" header_extension=".h" indent_amount="4" indent_symbol="space" is_template="0" language="python" mark_blocks="1" name="app" option="0" overwrite="1" path="/home/lex/repos/inkstitch/lib/gui/preferences.py" source_extension=".cpp" top_window="frame" use_gettext="1" use_new_namespace="1">
+ <object class="PreferencesFrame" name="frame" base="EditFrame">
+ <extracode_post>self.SetSizeHints(main_sizer.CalcMin())</extracode_post>
+ <title>Preferences</title>
+ <style>wxDEFAULT_FRAME_STYLE</style>
+ <object class="wxPanel" name="panel_1" base="EditPanel">
+ <object class="wxBoxSizer" name="main_sizer" base="EditBoxSizer">
+ <orient>wxVERTICAL</orient>
+ <object class="sizeritem">
+ <option>1</option>
+ <border>10</border>
+ <flag>wxALL|wxEXPAND</flag>
+ <object class="wxNotebook" name="notebook" base="EditNotebook">
+ <style>wxNB_TOP</style>
+ <tabs>
+ <tab window="this_svg_page">This SVG</tab>
+ <tab window="global_page">Global</tab>
+ </tabs>
+ <object class="wxPanel" name="this_svg_page" base="EditPanel">
+ <style>wxTAB_TRAVERSAL</style>
+ <object class="wxBoxSizer" name="sizer_1" base="EditBoxSizer">
+ <orient>wxVERTICAL</orient>
+ <object class="sizeritem">
+ <option>1</option>
+ <border>0</border>
+ <flag>wxEXPAND</flag>
+ <object class="spacer" name="spacer" base="EditSpacer">
+ <extracode_pre># add space above and below to center sizer_2 vertically</extracode_pre>
+ <width>0</width>
+ <height>0</height>
+ </object>
+ </object>
+ <object class="sizeritem">
+ <option>0</option>
+ <border>20</border>
+ <flag>wxLEFT|wxRIGHT|wxEXPAND</flag>
+ <object class="wxFlexGridSizer" name="sizer_2" base="EditFlexGridSizer">
+ <rows>2</rows>
+ <cols>4</cols>
+ <vgap>15</vgap>
+ <hgap>10</hgap>
+ <growable_cols>0</growable_cols>
+ <object class="sizeritem">
+ <option>1</option>
+ <border>15</border>
+ <flag>wxRIGHT|wxALIGN_CENTER_VERTICAL</flag>
+ <object class="wxStaticText" name="label_1" base="EditStaticText">
+ <tooltip>Jump stitches smaller than this will be treated as normal stitches.</tooltip>
+ <style>wxALIGN_LEFT</style>
+ <label>Minimum jump stitch length</label>
+ </object>
+ </object>
+ <object class="sizeritem">
+ <option>0</option>
+ <border>0</border>
+ <flag>wxALIGN_CENTER_VERTICAL</flag>
+ <object class="wxSpinCtrlDouble" name="minimum_jump_stitch_length" base="EditSpinCtrlDouble">
+ <style>wxSP_ARROW_KEYS|wxALIGN_RIGHT</style>
+ <value>0.0</value>
+ <digits>1</digits>
+ </object>
+ </object>
+ <object class="sizeritem">
+ <option>0</option>
+ <border>15</border>
+ <flag>wxRIGHT|wxALIGN_CENTER_VERTICAL</flag>
+ <object class="wxStaticText" name="label_2" base="EditStaticText">
+ <label>mm</label>
+ </object>
+ </object>
+ <object class="sizeritem">
+ <option>0</option>
+ <border>0</border>
+ <flag>wxALIGN_CENTER_VERTICAL</flag>
+ <object class="wxButton" name="button_1" base="EditButton">
+ <events>
+ <handler event="EVT_BUTTON">set_as_default_minimum_jump_stitch_length</handler>
+ </events>
+ <label>Set As Default</label>
+ </object>
+ </object>
+ <object class="sizeritem">
+ <option>0</option>
+ <border>0</border>
+ <object class="wxStaticText" name="label_3" base="EditStaticText">
+ <label>Minimum stitch length</label>
+ </object>
+ </object>
+ <object class="sizeritem">
+ <option>0</option>
+ <border>0</border>
+ <flag>wxALIGN_CENTER_VERTICAL</flag>
+ <object class="wxSpinCtrlDouble" name="minimum_stitch_length" base="EditSpinCtrlDouble">
+ <style>wxSP_ARROW_KEYS|wxALIGN_RIGHT</style>
+ <range>0.0, 100.0</range>
+ <value>0.0</value>
+ <digits>1</digits>
+ </object>
+ </object>
+ <object class="sizeritem">
+ <option>0</option>
+ <border>0</border>
+ <flag>wxALIGN_CENTER_VERTICAL</flag>
+ <object class="wxStaticText" name="label_4" base="EditStaticText">
+ <label>mm</label>
+ </object>
+ </object>
+ <object class="sizeritem">
+ <option>0</option>
+ <border>0</border>
+ <flag>wxALIGN_CENTER_VERTICAL</flag>
+ <object class="wxButton" name="button_2" base="EditButton">
+ <events>
+ <handler event="EVT_BUTTON">set_as_default_minimum_stitch_length</handler>
+ </events>
+ <label>Set As Default</label>
+ </object>
+ </object>
+ </object>
+ </object>
+ <object class="sizeritem">
+ <option>1</option>
+ <border>0</border>
+ <flag>wxEXPAND</flag>
+ <object class="spacer" name="spacer" base="EditSpacer">
+ <width>0</width>
+ <height>0</height>
+ </object>
+ </object>
+ </object>
+ </object>
+ <object class="wxPanel" name="global_page" base="EditPanel">
+ <style>wxTAB_TRAVERSAL</style>
+ <object class="wxBoxSizer" name="sizer_3" base="EditBoxSizer">
+ <orient>wxVERTICAL</orient>
+ <object class="sizeritem">
+ <option>1</option>
+ <border>0</border>
+ <flag>wxEXPAND</flag>
+ <object class="spacer" name="spacer" base="EditSpacer">
+ <extracode_pre># add space above and below to center sizer_4 vertically</extracode_pre>
+ <width>0</width>
+ <height>0</height>
+ </object>
+ </object>
+ <object class="sizeritem">
+ <option>0</option>
+ <border>20</border>
+ <flag>wxLEFT|wxRIGHT|wxEXPAND</flag>
+ <object class="wxFlexGridSizer" name="sizer_4" base="EditFlexGridSizer">
+ <rows>3</rows>
+ <cols>4</cols>
+ <vgap>15</vgap>
+ <hgap>10</hgap>
+ <growable_cols>0</growable_cols>
+ <object class="sizeritem">
+ <option>1</option>
+ <border>15</border>
+ <flag>wxRIGHT|wxALIGN_CENTER_VERTICAL</flag>
+ <object class="wxStaticText" name="label_5" base="EditStaticText">
+ <tooltip>Jump stitches smaller than this will be treated as normal stitches.</tooltip>
+ <style>wxALIGN_LEFT</style>
+ <label>Default minimum jump stitch length</label>
+ </object>
+ </object>
+ <object class="sizeritem">
+ <option>0</option>
+ <border>0</border>
+ <flag>wxALIGN_CENTER_VERTICAL</flag>
+ <object class="wxSpinCtrlDouble" name="default_minimum_jump_stitch_length_copy" base="EditSpinCtrlDouble">
+ <style>wxSP_ARROW_KEYS|wxALIGN_RIGHT</style>
+ <range>0.0, 100.0</range>
+ <value>0.0</value>
+ <digits>1</digits>
+ </object>
+ </object>
+ <object class="sizeritem">
+ <option>0</option>
+ <border>15</border>
+ <flag>wxRIGHT|wxALIGN_CENTER_VERTICAL</flag>
+ <object class="wxStaticText" name="label_6" base="EditStaticText">
+ <label>mm</label>
+ </object>
+ </object>
+ <object class="sizerslot" />
+ <object class="sizeritem">
+ <option>0</option>
+ <border>0</border>
+ <flag>wxALIGN_CENTER_VERTICAL</flag>
+ <object class="wxStaticText" name="label_7" base="EditStaticText">
+ <label>Minimum stitch length</label>
+ </object>
+ </object>
+ <object class="sizeritem">
+ <option>0</option>
+ <border>0</border>
+ <flag>wxALIGN_CENTER_VERTICAL</flag>
+ <object class="wxSpinCtrlDouble" name="default_minimum_stitch_length" base="EditSpinCtrlDouble">
+ <style>wxSP_ARROW_KEYS|wxALIGN_RIGHT</style>
+ <range>0.0, 100.0</range>
+ <value>0.0</value>
+ <digits>1</digits>
+ </object>
+ </object>
+ <object class="sizeritem">
+ <option>0</option>
+ <border>0</border>
+ <flag>wxALIGN_CENTER_VERTICAL</flag>
+ <object class="wxStaticText" name="label_8" base="EditStaticText">
+ <label>mm</label>
+ </object>
+ </object>
+ <object class="sizerslot" />
+ <object class="sizeritem">
+ <option>1</option>
+ <border>0</border>
+ <flag>wxALIGN_CENTER_VERTICAL</flag>
+ <object class="wxStaticText" name="label_9" base="EditStaticText">
+ <tooltip>Jump stitches smaller than this will be treated as normal stitches.</tooltip>
+ <style>wxALIGN_LEFT</style>
+ <label>Stitch plan cache size</label>
+ </object>
+ </object>
+ <object class="sizeritem">
+ <option>0</option>
+ <border>0</border>
+ <flag>wxALIGN_CENTER_VERTICAL</flag>
+ <object class="wxSpinCtrlDouble" name="stitch_plan_cache_size" base="EditSpinCtrlDouble">
+ <style>wxSP_ARROW_KEYS|wxALIGN_RIGHT</style>
+ <range>0.0, 100.0</range>
+ <value>0.0</value>
+ <digits>1</digits>
+ </object>
+ </object>
+ <object class="sizeritem">
+ <option>0</option>
+ <border>0</border>
+ <flag>wxALIGN_CENTER_VERTICAL</flag>
+ <object class="wxStaticText" name="label_10" base="EditStaticText">
+ <label>MB</label>
+ </object>
+ </object>
+ <object class="sizeritem">
+ <option>0</option>
+ <border>0</border>
+ <flag>wxALIGN_CENTER_VERTICAL</flag>
+ <object class="wxButton" name="button_3" base="EditButton">
+ <label>Clear Stitch Plan Cache</label>
+ </object>
+ </object>
+ </object>
+ </object>
+ <object class="sizeritem">
+ <option>1</option>
+ <border>0</border>
+ <flag>wxEXPAND</flag>
+ <object class="spacer" name="spacer" base="EditSpacer">
+ <width>0</width>
+ <height>0</height>
+ </object>
+ </object>
+ </object>
+ </object>
+ </object>
+ </object>
+ <object class="sizeritem">
+ <option>0</option>
+ <border>10</border>
+ <flag>wxLEFT|wxRIGHT|wxBOTTOM|wxEXPAND</flag>
+ <object class="wxBoxSizer" name="button_sizer" base="EditBoxSizer">
+ <orient>wxHORIZONTAL</orient>
+ <object class="sizeritem">
+ <option>1</option>
+ <border>0</border>
+ <object class="spacer" name="spacer" base="EditSpacer">
+ <width>0</width>
+ <height>0</height>
+ </object>
+ </object>
+ <object class="sizeritem">
+ <option>0</option>
+ <border>10</border>
+ <flag>wxRIGHT</flag>
+ <object class="wxButton" name="button_4" base="EditButton">
+ <label>button_4</label>
+ <stockitem>CANCEL</stockitem>
+ </object>
+ </object>
+ <object class="sizeritem">
+ <option>0</option>
+ <border>0</border>
+ <object class="wxButton" name="button_5" base="EditButton">
+ <label>button_5</label>
+ <stockitem>OK</stockitem>
+ </object>
+ </object>
+ </object>
+ </object>
+ </object>
+ </object>
+ </object>
+</application>
diff --git a/lib/gui/simulator.py b/lib/gui/simulator.py
index 78138fc8..e1357432 100644
--- a/lib/gui/simulator.py
+++ b/lib/gui/simulator.py
@@ -2,7 +2,7 @@
#
# Copyright (c) 2010 Authors
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
-
+import os
import sys
import time
from threading import Event, Thread
@@ -11,6 +11,7 @@ import wx
from wx.lib.intctrl import IntCtrl
from lib.debug import debug
+from lib.utils import get_resource_dir
from lib.utils.threading import ExitThread
from ..i18n import _
from ..stitch_plan import stitch_groups_to_stitch_plan, stitch_plan_from_file
@@ -29,6 +30,7 @@ COLOR_CHANGE = 4
class ControlPanel(wx.Panel):
""""""
+ @debug.time
def __init__(self, parent, *args, **kwargs):
""""""
self.parent = parent
@@ -38,44 +40,68 @@ class ControlPanel(wx.Panel):
kwargs['style'] = wx.BORDER_SUNKEN
wx.Panel.__init__(self, parent, *args, **kwargs)
- self.statusbar = self.GetTopLevelParent().statusbar
-
self.drawing_panel = None
self.num_stitches = 1
self.current_stitch = 1
self.speed = 1
self.direction = 1
+ self._last_color_block_end = 0
+
+ self.icons_dir = get_resource_dir("icons")
# Widgets
- self.btnMinus = wx.Button(self, -1, label='-')
+ self.button_size = self.GetTextExtent("M").y * 2
+ self.button_style = wx.BU_EXACTFIT | wx.BU_NOTEXT
+ self.btnMinus = wx.Button(self, -1, style=self.button_style)
self.btnMinus.Bind(wx.EVT_BUTTON, self.animation_slow_down)
+ self.btnMinus.SetBitmap(self.load_icon('slower'))
self.btnMinus.SetToolTip(_('Slow down (arrow down)'))
- self.btnPlus = wx.Button(self, -1, label='+')
+ self.btnPlus = wx.Button(self, -1, style=self.button_style)
self.btnPlus.Bind(wx.EVT_BUTTON, self.animation_speed_up)
+ self.btnPlus.SetBitmap(self.load_icon('faster'))
self.btnPlus.SetToolTip(_('Speed up (arrow up)'))
- self.btnBackwardStitch = wx.Button(self, -1, label='<|')
+ self.btnBackwardStitch = wx.Button(self, -1, style=self.button_style)
self.btnBackwardStitch.Bind(wx.EVT_BUTTON, self.animation_one_stitch_backward)
- self.btnBackwardStitch.SetToolTip(_('Go on step backward (-)'))
- self.btnForwardStitch = wx.Button(self, -1, label='|>')
+ self.btnBackwardStitch.SetBitmap(self.load_icon('backward_stitch'))
+ self.btnBackwardStitch.SetToolTip(_('Go backward one stitch (-)'))
+ self.btnForwardStitch = wx.Button(self, -1, style=self.button_style)
self.btnForwardStitch.Bind(wx.EVT_BUTTON, self.animation_one_stitch_forward)
- self.btnForwardStitch.SetToolTip(_('Go on step forward (+)'))
- self.directionBtn = wx.Button(self, -1, label='<<')
- self.directionBtn.Bind(wx.EVT_BUTTON, self.on_direction_button)
- self.directionBtn.SetToolTip(_('Switch direction (arrow left | arrow right)'))
- self.pauseBtn = wx.Button(self, -1, label=_('Pause'))
- self.pauseBtn.Bind(wx.EVT_BUTTON, self.on_pause_start_button)
- self.pauseBtn.SetToolTip(_('Pause (P)'))
- self.restartBtn = wx.Button(self, -1, label=_('Restart'))
- self.restartBtn.Bind(wx.EVT_BUTTON, self.animation_restart)
- self.restartBtn.SetToolTip(_('Restart (R)'))
- self.nppBtn = wx.ToggleButton(self, -1, label=_('O'))
- self.nppBtn.Bind(wx.EVT_TOGGLEBUTTON, self.toggle_npp)
- self.nppBtn.SetToolTip(_('Display needle penetration point (O)'))
- self.quitBtn = wx.Button(self, -1, label=_('Quit'))
- self.quitBtn.Bind(wx.EVT_BUTTON, self.animation_quit)
- self.quitBtn.SetToolTip(_('Quit (Q)'))
- self.slider = wx.Slider(self, -1, value=1, minValue=1, maxValue=2,
- style=wx.SL_HORIZONTAL | wx.SL_LABELS)
+ self.btnForwardStitch.SetBitmap(self.load_icon('forward_stitch'))
+ self.btnForwardStitch.SetToolTip(_('Go forward one stitch (+)'))
+ self.btnBackwardCommand = wx.Button(self, -1, style=self.button_style)
+ self.btnBackwardCommand.Bind(wx.EVT_BUTTON, self.animation_one_command_backward)
+ self.btnBackwardCommand.SetBitmap(self.load_icon('backward_command'))
+ self.btnBackwardCommand.SetToolTip(_('Go backward one command (page-down)'))
+ self.btnForwardCommand = wx.Button(self, -1, style=self.button_style)
+ self.btnForwardCommand.Bind(wx.EVT_BUTTON, self.animation_one_command_forward)
+ self.btnForwardCommand.SetBitmap(self.load_icon('forward_command'))
+ self.btnForwardCommand.SetToolTip(_('Go forward one command (page-up)'))
+ self.btnForward = wx.BitmapToggleButton(self, -1, style=self.button_style)
+ self.btnForward.SetValue(True)
+ self.btnForward.Bind(wx.EVT_TOGGLEBUTTON, self.on_forward_button)
+ self.btnForward.SetBitmap(self.load_icon('forward'))
+ self.btnForward.SetToolTip(_('Animate forward (arrow right)'))
+ self.btnReverse = wx.BitmapToggleButton(self, -1, style=self.button_style)
+ self.btnReverse.Bind(wx.EVT_TOGGLEBUTTON, self.on_reverse_button)
+ self.btnReverse.SetBitmap(self.load_icon('reverse'))
+ self.btnReverse.SetToolTip(_('Animate in reverse (arrow right)'))
+ self.btnPlay = wx.BitmapToggleButton(self, -1, style=self.button_style)
+ self.btnPlay.Bind(wx.EVT_TOGGLEBUTTON, self.on_play_button)
+ self.btnPlay.SetBitmap(self.load_icon('play'))
+ self.btnPlay.SetToolTip(_('Play (P)'))
+ self.btnPause = wx.BitmapToggleButton(self, -1, style=self.button_style)
+ self.btnPause.Bind(wx.EVT_TOGGLEBUTTON, self.on_pause_button)
+ self.btnPause.SetBitmap(self.load_icon('pause'))
+ self.btnPause.SetToolTip(_('Pause (P)'))
+ self.btnRestart = wx.Button(self, -1, style=self.button_style)
+ self.btnRestart.Bind(wx.EVT_BUTTON, self.animation_restart)
+ self.btnRestart.SetBitmap(self.load_icon('restart'))
+ self.btnRestart.SetToolTip(_('Restart (R)'))
+ self.btnNpp = wx.BitmapToggleButton(self, -1, style=self.button_style)
+ self.btnNpp.Bind(wx.EVT_TOGGLEBUTTON, self.toggle_npp)
+ self.btnNpp.SetBitmap(self.load_icon('npp'))
+ self.btnNpp.SetToolTip(_('Display needle penetration point (O)'))
+ self.slider = SimulatorSlider(self, -1, value=1, minValue=1, maxValue=2)
self.slider.Bind(wx.EVT_SLIDER, self.on_slider)
self.stitchBox = IntCtrl(self, -1, value=1, min=1, max=2, limited=True, allow_none=True, style=wx.TE_PROCESS_ENTER)
self.stitchBox.Bind(wx.EVT_LEFT_DOWN, self.on_stitch_box_focus)
@@ -83,24 +109,89 @@ class ControlPanel(wx.Panel):
self.stitchBox.Bind(wx.EVT_TEXT_ENTER, self.on_stitch_box_focusout)
self.stitchBox.Bind(wx.EVT_KILL_FOCUS, self.on_stitch_box_focusout)
self.Bind(wx.EVT_LEFT_DOWN, self.on_stitch_box_focusout)
+ self.btnJump = wx.BitmapToggleButton(self, -1, style=self.button_style)
+ self.btnJump.SetToolTip(_('Show jump stitches'))
+ self.btnJump.SetBitmap(self.load_icon('jump'))
+ self.btnJump.Bind(wx.EVT_TOGGLEBUTTON, lambda event: self.on_marker_button('jump', event))
+ self.btnTrim = wx.BitmapToggleButton(self, -1, style=self.button_style)
+ self.btnTrim.SetToolTip(_('Show trims'))
+ self.btnTrim.SetBitmap(self.load_icon('trim'))
+ self.btnTrim.Bind(wx.EVT_TOGGLEBUTTON, lambda event: self.on_marker_button('trim', event))
+ self.btnStop = wx.BitmapToggleButton(self, -1, style=self.button_style)
+ self.btnStop.SetToolTip(_('Show stops'))
+ self.btnStop.SetBitmap(self.load_icon('stop'))
+ self.btnStop.Bind(wx.EVT_TOGGLEBUTTON, lambda event: self.on_marker_button('stop', event))
+ self.btnColorChange = wx.BitmapToggleButton(self, -1, style=self.button_style)
+ self.btnColorChange.SetToolTip(_('Show color changes'))
+ self.btnColorChange.SetBitmap(self.load_icon('color_change'))
+ self.btnColorChange.Bind(wx.EVT_TOGGLEBUTTON, lambda event: self.on_marker_button('color_change', event))
# Layout
+ self.hbSizer1 = wx.BoxSizer(wx.HORIZONTAL)
+ self.hbSizer1.Add(self.slider, 1, wx.EXPAND | wx.RIGHT, 10)
+ self.hbSizer1.Add(self.stitchBox, 0, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 10)
+
+ self.command_sizer = wx.StaticBoxSizer(wx.StaticBox(self, wx.ID_ANY, _("Command")), wx.VERTICAL)
+ self.command_text = wx.StaticText(self, wx.ID_ANY, label="", style=wx.ALIGN_CENTRE_HORIZONTAL | wx.ST_NO_AUTORESIZE)
+ self.command_text.SetFont(wx.Font(wx.FontInfo(20).Bold()))
+ self.command_text.SetMinSize(self.get_max_command_text_size())
+ self.command_sizer.Add(self.command_text, 0, wx.EXPAND | wx.ALL, 10)
+ self.hbSizer1.Add(self.command_sizer, 0, wx.EXPAND)
+
+ self.controls_sizer = wx.StaticBoxSizer(wx.StaticBox(self, wx.ID_ANY, _("Controls")), wx.HORIZONTAL)
+ self.controls_inner_sizer = wx.BoxSizer(wx.HORIZONTAL)
+ self.controls_inner_sizer.Add(self.btnBackwardCommand, 0, wx.EXPAND | wx.ALL, 2)
+ self.controls_inner_sizer.Add(self.btnBackwardStitch, 0, wx.EXPAND | wx.ALL, 2)
+ self.controls_inner_sizer.Add(self.btnForwardStitch, 0, wx.EXPAND | wx.ALL, 2)
+ self.controls_inner_sizer.Add(self.btnForwardCommand, 0, wx.EXPAND | wx.ALL, 2)
+ self.controls_inner_sizer.Add(self.btnReverse, 0, wx.EXPAND | wx.ALL, 2)
+ self.controls_inner_sizer.Add(self.btnForward, 0, wx.EXPAND | wx.ALL, 2)
+ self.controls_inner_sizer.Add(self.btnPlay, 0, wx.EXPAND | wx.ALL, 2)
+ self.controls_inner_sizer.Add(self.btnPause, 0, wx.EXPAND | wx.ALL, 2)
+ self.controls_inner_sizer.Add(self.btnRestart, 0, wx.EXPAND | wx.ALL, 2)
+ self.controls_sizer.Add((1, 1), 1)
+ self.controls_sizer.Add(self.controls_inner_sizer, 0, wx.ALIGN_CENTER_VERTICAL | wx.ALL, 10)
+ self.controls_sizer.Add((1, 1), 1)
+
+ self.show_sizer = wx.StaticBoxSizer(wx.StaticBox(self, wx.ID_ANY, _("Show")), wx.HORIZONTAL)
+ self.show_inner_sizer = wx.BoxSizer(wx.HORIZONTAL)
+ self.show_inner_sizer.Add(self.btnNpp, 0, wx.EXPAND | wx.ALL, 2)
+ self.show_inner_sizer.Add(self.btnJump, 0, wx.ALL, 2)
+ self.show_inner_sizer.Add(self.btnTrim, 0, wx.ALL, 2)
+ self.show_inner_sizer.Add(self.btnStop, 0, wx.ALL, 2)
+ self.show_inner_sizer.Add(self.btnColorChange, 0, wx.ALL, 2)
+ self.show_sizer.Add((1, 1), 1)
+ self.show_sizer.Add(self.show_inner_sizer, 0, wx.ALIGN_CENTER_VERTICAL | wx.ALL, 10)
+ self.show_sizer.Add((1, 1), 1)
+
+ self.speed_sizer = wx.StaticBoxSizer(wx.StaticBox(self, wx.ID_ANY, _("Speed")), wx.VERTICAL)
+
+ self.speed_buttons_sizer = wx.BoxSizer(wx.HORIZONTAL)
+ self.speed_buttons_sizer.Add((1, 1), 1)
+ self.speed_buttons_sizer.Add(self.btnMinus, 0, wx.ALL, 2)
+ self.speed_buttons_sizer.Add(self.btnPlus, 0, wx.ALL, 2)
+ self.speed_buttons_sizer.Add((1, 1), 1)
+ self.speed_sizer.Add(self.speed_buttons_sizer, 0, wx.EXPAND | wx.ALL)
+ self.speed_text = wx.StaticText(self, wx.ID_ANY, label="", style=wx.ALIGN_CENTRE_HORIZONTAL | wx.ST_NO_AUTORESIZE)
+ self.speed_text.SetFont(wx.Font(wx.FontInfo(15).Bold()))
+ extent = self.speed_text.GetTextExtent(self.format_speed_text(100000))
+ self.speed_text.SetMinSize(extent)
+ self.speed_sizer.Add(self.speed_text, 0, wx.EXPAND | wx.ALL, 5)
+
+ # A normal BoxSizer can only make child components the same or
+ # proportional size. A FlexGridSizer can split up the available extra
+ # space evenly among all growable columns.
+ self.control_row2_sizer = wx.FlexGridSizer(cols=3, vgap=0, hgap=5)
+ self.control_row2_sizer.AddGrowableCol(0)
+ self.control_row2_sizer.AddGrowableCol(1)
+ self.control_row2_sizer.AddGrowableCol(2)
+ self.control_row2_sizer.Add(self.controls_sizer, 0, wx.EXPAND)
+ self.control_row2_sizer.Add(self.speed_sizer, 0, wx.EXPAND)
+ self.control_row2_sizer.Add(self.show_sizer, 0, wx.EXPAND)
+
self.vbSizer = vbSizer = wx.BoxSizer(wx.VERTICAL)
- self.hbSizer1 = hbSizer1 = wx.BoxSizer(wx.HORIZONTAL)
- self.hbSizer2 = hbSizer2 = wx.BoxSizer(wx.HORIZONTAL)
- hbSizer1.Add(self.slider, 1, wx.EXPAND | wx.ALL, 3)
- hbSizer1.Add(self.stitchBox, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 2)
- vbSizer.Add(hbSizer1, 1, wx.EXPAND | wx.ALL, 3)
- hbSizer2.Add(self.btnMinus, 0, wx.EXPAND | wx.ALL, 2)
- hbSizer2.Add(self.btnPlus, 0, wx.EXPAND | wx.ALL, 2)
- hbSizer2.Add(self.btnBackwardStitch, 0, wx.EXPAND | wx.ALL, 2)
- hbSizer2.Add(self.btnForwardStitch, 0, wx.EXPAND | wx.ALL, 2)
- hbSizer2.Add(self.directionBtn, 0, wx.EXPAND | wx.ALL, 2)
- hbSizer2.Add(self.pauseBtn, 0, wx.EXPAND | wx.ALL, 2)
- hbSizer2.Add(self.restartBtn, 0, wx.EXPAND | wx.ALL, 2)
- hbSizer2.Add(self.nppBtn, 0, wx.EXPAND | wx.ALL, 2)
- hbSizer2.Add(self.quitBtn, 0, wx.EXPAND | wx.ALL, 2)
- vbSizer.Add(hbSizer2, 0, wx.EXPAND | wx.ALL, 3)
+ vbSizer.Add(self.hbSizer1, 1, wx.EXPAND | wx.ALL, 10)
+ vbSizer.Add(self.control_row2_sizer, 0, wx.EXPAND | wx.LEFT | wx.RIGHT | wx.BOTTOM, 10)
self.SetSizerAndFit(vbSizer)
# Keyboard Shortcuts
@@ -125,9 +216,13 @@ class ControlPanel(wx.Panel):
(wx.ACCEL_NORMAL, wx.WXK_NUMPAD_SUBTRACT, self.animation_one_stitch_backward),
(wx.ACCEL_NORMAL, ord('r'), self.animation_restart),
(wx.ACCEL_NORMAL, ord('o'), self.on_toggle_npp_shortcut),
- (wx.ACCEL_NORMAL, ord('p'), self.on_pause_start_button),
- (wx.ACCEL_NORMAL, wx.WXK_SPACE, self.on_pause_start_button),
- (wx.ACCEL_NORMAL, ord('q'), self.animation_quit)]
+ (wx.ACCEL_NORMAL, ord('p'), self.play_or_pause),
+ (wx.ACCEL_NORMAL, wx.WXK_SPACE, self.play_or_pause),
+ (wx.ACCEL_NORMAL, ord('q'), self.animation_quit),
+ (wx.ACCEL_NORMAL, wx.WXK_PAGEDOWN, self.animation_one_command_backward),
+ (wx.ACCEL_NORMAL, wx.WXK_PAGEUP, self.animation_one_command_forward),
+
+ ]
self.accel_entries = []
@@ -140,11 +235,14 @@ class ControlPanel(wx.Panel):
self.SetAcceleratorTable(self.accel_table)
self.SetFocus()
+ # wait for layouts so that panel size is set
+ wx.CallLater(50, self.load, self.stitch_plan)
+
def set_drawing_panel(self, drawing_panel):
self.drawing_panel = drawing_panel
self.drawing_panel.set_speed(self.speed)
- def set_num_stitches(self, num_stitches):
+ def _set_num_stitches(self, num_stitches):
if num_stitches < 2:
# otherwise the slider and intctrl get mad
num_stitches = 2
@@ -153,6 +251,41 @@ class ControlPanel(wx.Panel):
self.slider.SetMax(num_stitches)
self.choose_speed()
+ def add_color(self, color, num_stitches):
+ start = self._last_color_block_end + 1
+ self.slider.add_color_section(ColorSection(color.rgb, start, start + num_stitches - 1))
+ self._last_color_block_end = self._last_color_block_end + num_stitches
+
+ def load(self, stitch_plan):
+ self.stitches = []
+ self._set_num_stitches(stitch_plan.num_stitches)
+
+ stitch_num = 0
+ for color_block in stitch_plan.color_blocks:
+ self.stitches.extend(color_block.stitches)
+
+ start = stitch_num + 1
+ end = start + color_block.num_stitches
+ self.slider.add_color_section(color_block.color.rgb, start, end)
+
+ for stitch_num, stitch in enumerate(color_block.stitches, start):
+ if stitch.trim:
+ self.slider.add_marker("trim", stitch_num)
+ elif stitch.stop:
+ self.slider.add_marker("stop", stitch_num)
+ elif stitch.jump:
+ self.slider.add_marker("jump", stitch_num)
+ elif stitch.color_change:
+ self.slider.add_marker("color_change", stitch_num)
+
+ def load_icon(self, icon_name):
+ icon = wx.Image(os.path.join(self.icons_dir, f"{icon_name}.png"))
+ icon.Rescale(self.button_size, self.button_size, wx.IMAGE_QUALITY_HIGH)
+ return icon.ConvertToBitmap()
+
+ def on_marker_button(self, marker_type, event):
+ self.slider.enable_marker_list(marker_type, event.GetEventObject().GetValue())
+
def choose_speed(self):
if self.target_duration:
self.set_speed(int(self.num_stitches / float(self.target_duration)))
@@ -160,22 +293,24 @@ class ControlPanel(wx.Panel):
self.set_speed(self.target_stitches_per_second)
def animation_forward(self, event=None):
- self.directionBtn.SetLabel("<<")
+ self.btnForward.SetValue(True)
+ self.btnReverse.SetValue(False)
self.drawing_panel.forward()
self.direction = 1
self.update_speed_text()
def animation_reverse(self, event=None):
- self.directionBtn.SetLabel(">>")
+ self.btnForward.SetValue(False)
+ self.btnReverse.SetValue(True)
self.drawing_panel.reverse()
self.direction = -1
self.update_speed_text()
- def on_direction_button(self, event):
- if self.direction == 1:
- self.animation_reverse()
- else:
- self.animation_forward()
+ def on_forward_button(self, event):
+ self.animation_forward()
+
+ def on_reverse_button(self, event):
+ self.animation_reverse()
def set_speed(self, speed):
self.speed = int(max(speed, 1))
@@ -184,9 +319,15 @@ class ControlPanel(wx.Panel):
if self.drawing_panel:
self.drawing_panel.set_speed(self.speed)
+ def format_speed_text(self, speed):
+ return _('%d stitches/sec') % speed
+
def update_speed_text(self):
- self.statusbar.SetStatusText(_('Speed: %d stitches/sec') % (self.speed * self.direction), 0)
- self.hbSizer2.Layout()
+ self.speed_text.SetLabel(self.format_speed_text(self.speed * self.direction))
+
+ def get_max_command_text_size(self):
+ extents = [self.command_text.GetTextExtent(command) for command in COMMAND_NAMES]
+ return max(extents, key=lambda extent: extent.x)
def on_slider(self, event):
stitch = event.GetEventObject().GetValue()
@@ -202,7 +343,7 @@ class ControlPanel(wx.Panel):
self.current_stitch = stitch
self.slider.SetValue(stitch)
self.stitchBox.SetValue(stitch)
- self.statusbar.SetStatusText(COMMAND_NAMES[command], 1)
+ self.command_text.SetLabel(COMMAND_NAMES[command])
def on_stitch_box_focus(self, event):
self.animation_pause()
@@ -238,14 +379,23 @@ class ControlPanel(wx.Panel):
self.drawing_panel.go()
def on_start(self):
- self.pauseBtn.SetLabel(_('Pause'))
+ self.btnPause.SetValue(False)
+ self.btnPlay.SetValue(True)
def on_stop(self):
- self.pauseBtn.SetLabel(_('Start'))
+ self.btnPause.SetValue(True)
+ self.btnPlay.SetValue(False)
- def on_pause_start_button(self, event):
+ def on_pause_button(self, event):
""""""
- if self.pauseBtn.GetLabel() == _('Pause'):
+ self.animation_pause()
+
+ def on_play_button(self, event):
+ """"""
+ self.animation_start()
+
+ def play_or_pause(self, event):
+ if self.drawing_panel.animating:
self.animation_pause()
else:
self.animation_start()
@@ -258,6 +408,28 @@ class ControlPanel(wx.Panel):
self.animation_pause()
self.drawing_panel.one_stitch_backward()
+ def animation_one_command_backward(self, event):
+ self.animation_pause()
+ stitch_number = self.current_stitch - 1
+ while stitch_number >= 1:
+ # stitch number shown to the user starts at 1
+ stitch = self.stitches[stitch_number - 1]
+ if stitch.jump or stitch.trim or stitch.stop or stitch.color_change:
+ break
+ stitch_number -= 1
+ self.drawing_panel.set_current_stitch(stitch_number)
+
+ def animation_one_command_forward(self, event):
+ self.animation_pause()
+ stitch_number = self.current_stitch + 1
+ while stitch_number <= self.num_stitches:
+ # stitch number shown to the user starts at 1
+ stitch = self.stitches[stitch_number - 1]
+ if stitch.jump or stitch.trim or stitch.stop or stitch.color_change:
+ break
+ stitch_number += 1
+ self.drawing_panel.set_current_stitch(stitch_number)
+
def animation_quit(self, event):
self.parent.quit()
@@ -265,13 +437,11 @@ class ControlPanel(wx.Panel):
self.drawing_panel.restart()
def on_toggle_npp_shortcut(self, event):
- self.nppBtn.SetValue(not self.nppBtn.GetValue())
+ self.btnNpp.SetValue(not self.btnNpp.GetValue())
self.toggle_npp(event)
def toggle_npp(self, event):
- if self.pauseBtn.GetLabel() == _('Start'):
- stitch = self.stitchBox.GetValue()
- self.drawing_panel.set_current_stitch(stitch)
+ self.drawing_panel.Refresh()
class DrawingPanel(wx.Panel):
@@ -405,8 +575,8 @@ class DrawingPanel(wx.Panel):
canvas.SetTransform(canvas.CreateMatrix())
crosshair_radius = 10
canvas.SetPen(self.black_pen)
- canvas.DrawLines(((x - crosshair_radius, y), (x + crosshair_radius, y)))
- canvas.DrawLines(((x, y - crosshair_radius), (x, y + crosshair_radius)))
+ canvas.StrokeLines(((x - crosshair_radius, y), (x + crosshair_radius, y)))
+ canvas.StrokeLines(((x, y - crosshair_radius), (x, y + crosshair_radius)))
def draw_scale(self, canvas):
canvas.BeginLayer(1)
@@ -433,13 +603,13 @@ class DrawingPanel(wx.Panel):
scale_lower_left_x = 20
scale_lower_left_y = canvas_height - 30
- canvas.DrawLines(((scale_lower_left_x, scale_lower_left_y - 6),
- (scale_lower_left_x, scale_lower_left_y),
- (scale_lower_left_x + scale_width / 2.0, scale_lower_left_y),
- (scale_lower_left_x + scale_width / 2.0, scale_lower_left_y - 3),
- (scale_lower_left_x + scale_width / 2.0, scale_lower_left_y),
- (scale_lower_left_x + scale_width, scale_lower_left_y),
- (scale_lower_left_x + scale_width, scale_lower_left_y - 5)))
+ canvas.StrokeLines(((scale_lower_left_x, scale_lower_left_y - 6),
+ (scale_lower_left_x, scale_lower_left_y),
+ (scale_lower_left_x + scale_width / 2.0, scale_lower_left_y),
+ (scale_lower_left_x + scale_width / 2.0, scale_lower_left_y - 3),
+ (scale_lower_left_x + scale_width / 2.0, scale_lower_left_y),
+ (scale_lower_left_x + scale_width, scale_lower_left_y),
+ (scale_lower_left_x + scale_width, scale_lower_left_y - 6)))
canvas.SetFont(wx.Font(12, wx.DEFAULT, wx.NORMAL, wx.NORMAL), wx.Colour((0, 0, 0)))
canvas.DrawText("%s mm" % scale_width_mm, scale_lower_left_x, scale_lower_left_y + 5)
@@ -447,7 +617,7 @@ class DrawingPanel(wx.Panel):
canvas.EndLayer()
def draw_needle_penetration_points(self, canvas, pen, stitches):
- if self.control_panel.nppBtn.GetValue():
+ if self.control_panel.btnNpp.GetValue():
npp_pen = wx.Pen(pen.GetColour(), width=int(0.5 * PIXELS_PER_MM * self.PIXEL_DENSITY))
canvas.SetPen(npp_pen)
canvas.StrokeLineSegments(stitches, [(stitch[0] + 0.001, stitch[1]) for stitch in stitches])
@@ -460,11 +630,10 @@ class DrawingPanel(wx.Panel):
self.current_stitch = 1
self.direction = 1
self.last_frame_duration = 0
- self.num_stitches = stitch_plan.num_stitches
- self.control_panel.set_num_stitches(self.num_stitches)
self.minx, self.miny, self.maxx, self.maxy = stitch_plan.bounding_box
self.width = self.maxx - self.minx
self.height = self.maxy - self.miny
+ self.num_stitches = stitch_plan.num_stitches
self.parse_stitch_plan(stitch_plan)
self.choose_zoom_and_pan()
self.set_current_stitch(0)
@@ -640,6 +809,141 @@ class DrawingPanel(wx.Panel):
self.Refresh()
+class MarkerList(list):
+ def __init__(self, icon_name, stitch_numbers=()):
+ super().__init__(self)
+ icons_dir = get_resource_dir("icons")
+ self.icon_name = icon_name
+ self.icon = wx.Image(os.path.join(icons_dir, f"{icon_name}.png")).ConvertToBitmap()
+ self.enabled = False
+ self.extend(stitch_numbers)
+
+ def __repr__(self):
+ return f"MarkerList({self.icon_name})"
+
+
+class ColorSection:
+ def __init__(self, color, start, end):
+ self.color = color
+ self.start = start
+ self.end = end
+ self.brush = wx.Brush(wx.Colour(*color))
+
+
+class SimulatorSlider(wx.Panel):
+ PROXY_EVENTS = (wx.EVT_SLIDER,)
+
+ def __init__(self, parent, id=wx.ID_ANY, *args, **kwargs):
+ super().__init__(parent, id)
+
+ kwargs['style'] = wx.SL_HORIZONTAL | wx.SL_LABELS
+
+ self.sizer = wx.BoxSizer(wx.VERTICAL)
+ self.slider = wx.Slider(self, *args, **kwargs)
+ self.sizer.Add(self.slider, 0, wx.EXPAND)
+
+ # add 33% additional vertical space for marker icons
+ size = self.sizer.CalcMin()
+ self.sizer.Add((10, size.height // 3), 1, wx.EXPAND)
+ self.SetSizerAndFit(self.sizer)
+
+ self.marker_lists = {
+ "trim": MarkerList("trim"),
+ "stop": MarkerList("stop"),
+ "jump": MarkerList("jump"),
+ "color_change": MarkerList("color_change"),
+ }
+ self.marker_pen = wx.Pen(wx.Colour(0, 0, 0))
+ self.color_sections = []
+ self.margin = 13
+ self.color_bar_start = 0.25
+ self.color_bar_thickness = 0.25
+ self.marker_start = 0.375
+ self.marker_end = 0.75
+ self.marker_icon_start = 0.75
+ self.marker_icon_size = size.height // 3
+
+ self.Bind(wx.EVT_PAINT, self.on_paint)
+ self.Bind(wx.EVT_ERASE_BACKGROUND, self.on_erase_background)
+
+ def SetMax(self, value):
+ self.slider.SetMax(value)
+
+ def SetMin(self, value):
+ self.slider.SetMin(value)
+
+ def SetValue(self, value):
+ self.slider.SetValue(value)
+
+ def Bind(self, event, callback, *args, **kwargs):
+ if event in self.PROXY_EVENTS:
+ self.slider.Bind(event, callback, *args, **kwargs)
+ else:
+ super().Bind(event, callback, *args, **kwargs)
+
+ def add_color_section(self, color, start, end):
+ self.color_sections.append(ColorSection(color, start, end))
+
+ def add_marker(self, name, location):
+ self.marker_lists[name].append(location)
+ self.Refresh()
+
+ def enable_marker_list(self, name, enabled=True):
+ self.marker_lists[name].enabled = enabled
+ self.Refresh()
+
+ def disable_marker_list(self, name):
+ self.marker_lists[name].enabled = False
+ self.Refresh()
+
+ def toggle_marker_list(self, name):
+ self.marker_lists[name].enabled = not self.marker_lists[name].enabled
+ self.Refresh()
+
+ def on_paint(self, event):
+ dc = wx.BufferedPaintDC(self)
+ background_brush = wx.Brush(self.GetTopLevelParent().GetBackgroundColour(), wx.SOLID)
+ dc.SetBackground(background_brush)
+ dc.Clear()
+ gc = wx.GraphicsContext.Create(dc)
+
+ width, height = self.GetSize()
+ min_value = self.slider.GetMin()
+ max_value = self.slider.GetMax()
+ spread = max_value - min_value
+
+ def _value_to_x(value):
+ return (value - min_value) * (width - 2 * self.margin) / spread + self.margin
+
+ gc.SetPen(wx.NullPen)
+ for color_section in self.color_sections:
+ gc.SetBrush(color_section.brush)
+
+ start_x = _value_to_x(color_section.start)
+ end_x = _value_to_x(color_section.end)
+ gc.DrawRectangle(start_x, height * self.color_bar_start,
+ end_x - start_x, height * self.color_bar_thickness)
+
+ gc.SetPen(self.marker_pen)
+ for marker_list in self.marker_lists.values():
+ if marker_list.enabled:
+ for value in marker_list:
+ x = _value_to_x(value)
+ gc.StrokeLine(
+ x, height * self.marker_start,
+ x, height * self.marker_end
+ )
+ gc.DrawBitmap(
+ marker_list.icon,
+ x - self.marker_icon_size / 2, height * self.marker_icon_start,
+ self.marker_icon_size, self.marker_icon_size
+ )
+
+ def on_erase_background(self, event):
+ # supposedly this prevents flickering?
+ pass
+
+
class SimulatorPanel(wx.Panel):
""""""
@@ -675,6 +979,7 @@ class SimulatorPanel(wx.Panel):
def load(self, stitch_plan):
self.dp.load(stitch_plan)
+ self.cp.load(stitch_plan)
def clear(self):
self.dp.clear()
@@ -687,8 +992,6 @@ class EmbroiderySimulator(wx.Frame):
stitches_per_second = kwargs.pop('stitches_per_second', 16)
target_duration = kwargs.pop('target_duration', None)
wx.Frame.__init__(self, *args, **kwargs)
- self.statusbar = self.CreateStatusBar(2)
- self.statusbar.SetStatusWidths([250, -1])
sizer = wx.BoxSizer(wx.HORIZONTAL)
self.simulator_panel = SimulatorPanel(self,
@@ -839,6 +1142,8 @@ class SimulatorPreview(Thread):
on_close=self.simulate_window_closed,
target_duration=self.target_duration)
except Exception:
+ import traceback
+ print(traceback.format_exc(), file=sys.stderr)
try:
# a window may have been created, so we need to destroy it
# or the app will never exit
diff --git a/lib/gui/warnings.py b/lib/gui/warnings.py
index 48788652..eda1ca2e 100644
--- a/lib/gui/warnings.py
+++ b/lib/gui/warnings.py
@@ -15,14 +15,25 @@ class WarningPanel(wx.Panel):
def __init__(self, parent, *args, **kwargs):
wx.Panel.__init__(self, parent, wx.ID_ANY, *args, **kwargs)
- self.warning_box = wx.StaticBox(self, wx.ID_ANY)
+ self.main_sizer = wx.BoxSizer(wx.VERTICAL)
self.warning = wx.StaticText(self)
- self.warning.SetLabel(_("Cannot load simulator.\nClose Params to get full error message."))
+ self.warning.SetLabel(_("An error occurred while rendering the stitch plan:"))
self.warning.SetForegroundColour(wx.Colour(255, 25, 25))
+ self.main_sizer.Add(self.warning, 1, wx.LEFT | wx.BOTTOM | wx.EXPAND, 10)
- warning_sizer = wx.StaticBoxSizer(self.warning_box, wx.HORIZONTAL)
- warning_sizer.Add(self.warning, 1, wx.LEFT | wx.BOTTOM | wx.EXPAND, 10)
+ tc_style = wx.TE_MULTILINE | wx.TE_READONLY | wx.VSCROLL | wx.TE_RICH2
+ self.warning_text = wx.TextCtrl(self, size=(300, 100), style=tc_style)
+ font = self.warning_text.GetFont()
+ font.SetFamily(wx.FONTFAMILY_TELETYPE)
+ self.warning_text.SetFont(font)
+ self.main_sizer.Add(self.warning_text, 3, wx.LEFT | wx.BOTTOM | wx.EXPAND, 10)
- self.SetSizerAndFit(warning_sizer)
+ self.SetSizerAndFit(self.main_sizer)
self.Layout()
+
+ def set_warning_text(self, text):
+ self.warning_text.SetValue(text)
+
+ def clear(self):
+ self.warning_text.SetValue("")
diff --git a/lib/stitch_plan/color_block.py b/lib/stitch_plan/color_block.py
index 9d474f80..fdef5eb8 100644
--- a/lib/stitch_plan/color_block.py
+++ b/lib/stitch_plan/color_block.py
@@ -3,10 +3,12 @@
# 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 typing import List
+
+from ..svg import PIXELS_PER_MM
from ..threads import ThreadColor
from ..utils.geometry import Point
-from ..svg import PIXELS_PER_MM
+from .stitch import Stitch
class ColorBlock(object):
@@ -155,3 +157,20 @@ class ColorBlock(object):
maxy = max(stitch.y for stitch in self)
return minx, miny, maxx, maxy
+
+ def make_offsets(self, offsets: List[Point]):
+ first_final_stitch = len(self.stitches)
+ while (first_final_stitch > 0 and self.stitches[first_final_stitch-1].is_terminator):
+ first_final_stitch -= 1
+ if first_final_stitch == 0:
+ return self
+ final_stitches = self.stitches[first_final_stitch:]
+ block_stitches = self.stitches[:first_final_stitch]
+
+ out = ColorBlock(self.color)
+ for i, offset in enumerate(offsets):
+ out.add_stitches([s.offset(offset) for s in block_stitches])
+ if i != len(offsets) - 1:
+ out.add_stitch(trim=True)
+ out.add_stitches(final_stitches)
+ return out
diff --git a/lib/stitch_plan/stitch.py b/lib/stitch_plan/stitch.py
index 90af58c0..8ad699c7 100644
--- a/lib/stitch_plan/stitch.py
+++ b/lib/stitch_plan/stitch.py
@@ -68,6 +68,10 @@ class Stitch(Point):
if value or base_stitch is None:
setattr(self, attribute, value)
+ @property
+ def is_terminator(self) -> bool:
+ return self.trim or self.stop or self.color_change
+
def add_tags(self, tags):
for tag in tags:
self.add_tag(tag)
@@ -93,6 +97,12 @@ class Stitch(Point):
def copy(self):
return Stitch(self.x, self.y, self.color, self.jump, self.stop, self.trim, self.color_change, self.tags)
+ def offset(self, offset: Point):
+ out = self.copy()
+ out.x += offset.x
+ out.y += offset.y
+ return out
+
def __json__(self):
attributes = dict(vars(self))
attributes['tags'] = list(attributes['tags'])
diff --git a/lib/stitch_plan/stitch_plan.py b/lib/stitch_plan/stitch_plan.py
index 25571578..caea9c09 100644
--- a/lib/stitch_plan/stitch_plan.py
+++ b/lib/stitch_plan/stitch_plan.py
@@ -4,13 +4,15 @@
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
from sys import exit
+from typing import List
from inkex import errormsg
from ..i18n import _
from ..svg import PIXELS_PER_MM
-from .color_block import ColorBlock
+from ..utils.geometry import Point
from ..utils.threading import check_stop_flag
+from .color_block import ColorBlock
def stitch_groups_to_stitch_plan(stitch_groups, collapse_len=None, min_stitch_len=0.1, disable_ties=False): # noqa: C901
@@ -207,3 +209,8 @@ class StitchPlan(object):
return self.color_blocks[-1]
else:
return None
+
+ def make_offsets(self, offsets: List[Point]):
+ out = StitchPlan()
+ out.color_blocks = [block.make_offsets(offsets) for block in self]
+ return out
diff --git a/lib/stitches/meander_fill.py b/lib/stitches/meander_fill.py
index f6106606..c1308bf4 100644
--- a/lib/stitches/meander_fill.py
+++ b/lib/stitches/meander_fill.py
@@ -1,7 +1,6 @@
from itertools import combinations
import networkx as nx
-from inkex import errormsg
from shapely.geometry import LineString, MultiPoint, Point
from shapely.ops import nearest_points
@@ -30,10 +29,8 @@ def meander_fill(fill, shape, original_shape, shape_index, starting_point, endin
graph = tile.to_graph(shape, fill.meander_scale, fill.meander_angle)
if not graph:
- label = fill.node.label or fill.node.get_id()
- errormsg(_('%s: Could not build graph for meander stitching. Try to enlarge your shape or '
- 'scale your meander pattern down.') % label)
- return []
+ fill.fatal(_('Could not build graph for meander stitching. Try to enlarge your shape or '
+ 'scale your meander pattern down.'))
debug.log_graph(graph, 'Meander graph')
ensure_connected(graph)
diff --git a/lib/svg/path.py b/lib/svg/path.py
index 6c2cbe35..878d2a7c 100644
--- a/lib/svg/path.py
+++ b/lib/svg/path.py
@@ -53,11 +53,18 @@ def get_node_transform(node):
def get_correction_transform(node, child=False):
- """Get a transform to apply to new siblings or children of this SVG node"""
+ """Get a transform to apply to new siblings or children of this SVG node
- # if we want to place our new nodes in the same group/layer as this node,
- # then we'll need to factor in the effects of any transforms set on
- # the parents of this node.
+ Arguments:
+ child (boolean) -- whether the new nodes we're going to add will be
+ children of node (child=True) or siblings of node
+ (child=False)
+
+ This allows us to add a new child node that has its path specified in
+ absolute coordinates. The correction transform will undo the effects of
+ the parent's and ancestors' transforms so that absolute coordinates
+ work properly.
+ """
if child:
transform = get_node_transform(node)
diff --git a/lib/threads/color.py b/lib/threads/color.py
index 8dc1ea01..44fa709c 100644
--- a/lib/threads/color.py
+++ b/lib/threads/color.py
@@ -6,19 +6,24 @@
import colorsys
import re
-import tinycss2.color3
-from pyembroidery.EmbThread import EmbThread
-
from inkex import Color
+from pyembroidery.EmbThread import EmbThread
class ThreadColor(object):
hex_str_re = re.compile('#([0-9a-z]{3}|[0-9a-z]{6})', re.I)
def __init__(self, color, name=None, number=None, manufacturer=None, description=None, chart=None):
- # set colors with a gradient to black (avoiding an error message)
- if type(color) == str and color.startswith('url'):
+ '''
+ avoid error messages:
+ * set colors with a gradient to black
+ * currentColor/context-fill/context-stroke: should not just be black, but we want to avoid
+ error messages until inkex will be able to handle these css properties
+ '''
+ if type(color) == str and color.startswith(('url', 'currentColor', 'context')):
color = None
+ elif type(color) == str and color.startswith('rgb'):
+ color = tuple(int(value) for value in color[4:-1].split(','))
if color is None:
self.rgb = (0, 0, 0)
@@ -31,9 +36,7 @@ class ThreadColor(object):
self.rgb = (color.get_red(), color.get_green(), color.get_blue())
return
elif isinstance(color, str):
- self.rgb = tinycss2.color3.parse_color(color)
- # remove alpha channel and multiply with 255
- self.rgb = tuple(channel * 255.0 for channel in list(self.rgb)[:-1])
+ self.rgb = Color.parse_str(color)[1]
elif isinstance(color, (list, tuple)):
self.rgb = tuple(color)
elif self.hex_str_re.match(color):