diff options
Diffstat (limited to 'lib')
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): |
