From af96d720e9340e02b1ec6dafe10bf9a47e045804 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Thu, 7 Sep 2023 13:25:47 -0400 Subject: [PATCH] improve params errors (#2437) --- electron/src/renderer/assets/js/simulator.js | 20 +++++- .../src/renderer/assets/style/simulator.css | 4 +- .../src/renderer/components/Simulator.vue | 13 ++++ inkstitch.py | 24 +++---- lib/api/stitch_plan.py | 20 +++--- lib/elements/element.py | 55 +++++++++------ lib/elements/fill_stitch.py | 68 ++++++------------- lib/exceptions.py | 30 ++++++++ lib/extensions/params.py | 20 +++--- lib/gui/warnings.py | 21 ++++-- lib/stitches/meander_fill.py | 7 +- 11 files changed, 166 insertions(+), 116 deletions(-) diff --git a/electron/src/renderer/assets/js/simulator.js b/electron/src/renderer/assets/js/simulator.js index 8a0423f36..e751f137c 100644 --- a/electron/src/renderer/assets/js/simulator.js +++ b/electron/src/renderer/assets/js/simulator.js @@ -54,7 +54,9 @@ export default { showNeedlePenetrationPoints: false, renderJumps: true, showRealisticPreview: false, - showCursor: true + showCursor: true, + error: false, + error_message: "" } }, watch: { @@ -543,6 +545,9 @@ export default { zoomPage () { this.svg.viewbox(this.page_specs.bbox.x, this.page_specs.bbox.y - 50, this.page_specs.bbox.width + 100, this.page_specs.bbox.height + 100) this.resizeCursor() + }, + close () { + window.close() } }, created: function () { @@ -642,6 +647,19 @@ export default { }) this.start() + }).catch(error => { + this.loading = false + if (error.response) { + // Stitch plan generation had an error. Show it to the user. + this.error_message = error.response.data.error_message + } else if (error.request) { + // We sent the request and didn't get a response. + this.error_message = "Stitch plan generation failed." + } else { + // Something weird happened in axios. + this.error_message = error.message + } + this.error = true }) } } diff --git a/electron/src/renderer/assets/style/simulator.css b/electron/src/renderer/assets/style/simulator.css index e938cbe3a..1ce61865c 100644 --- a/electron/src/renderer/assets/style/simulator.css +++ b/electron/src/renderer/assets/style/simulator.css @@ -26,12 +26,12 @@ padding: 1rem; } -button { +.controls button { color: rgb(0, 51, 153); align-items: flex-start; text-align: center; cursor: default; - background: linear-gradient(0deg, rgba(169,169,169,1) 0%, rgba(255,255,255,1) 68%, rgba(227,227,227,1) 100%); + background: linear-gradient(0deg, rgba(169, 169, 169, 1) 0%, rgba(255, 255, 255, 1) 68%, rgba(227, 227, 227, 1) 100%); box-sizing: border-box; padding: 2px 6px 3px; border-width: 2px; diff --git a/electron/src/renderer/components/Simulator.vue b/electron/src/renderer/components/Simulator.vue index 6d76b133d..e2d65ed88 100644 --- a/electron/src/renderer/components/Simulator.vue +++ b/electron/src/renderer/components/Simulator.vue @@ -306,6 +306,19 @@ + + + + Error Generating Stitch Plan + + +

{{ error_message }}

+
+ + Close + +
+
diff --git a/inkstitch.py b/inkstitch.py index 1dc5a3e32..91a0f18a6 100644 --- a/inkstitch.py +++ b/inkstitch.py @@ -7,10 +7,11 @@ import pstats import logging import os import sys -import traceback from argparse import ArgumentParser from io import StringIO +from lib.exceptions import InkstitchException, format_uncaught_exception + if getattr(sys, 'frozen', None) is None: # When running in development mode, we want to use the inkex installed by # pip install, not the one bundled with Inkscape which is not new enough. @@ -28,7 +29,7 @@ from lxml.etree import XMLSyntaxError import lib.debug as debug from lib import extensions from lib.i18n import _ -from lib.utils import restore_stderr, save_stderr, version +from lib.utils import restore_stderr, save_stderr # ignore warnings in releases if getattr(sys, 'frozen', None): @@ -90,24 +91,15 @@ else: msg += "\n\n" msg += _("Try to import the file into Inkscape through 'File > Import...' (Ctrl+I)") errormsg(msg) + except InkstitchException as exc: + errormsg(str(exc)) except Exception: - exception = traceback.format_exc() + errormsg(format_uncaught_exception()) + sys.exit(1) finally: restore_stderr() if shapely_errors.tell(): errormsg(shapely_errors.getvalue()) - if exception: - errormsg(_("Ink/Stitch experienced an unexpected error. This means it is a bug in Ink/Stitch.") + "\n") - errormsg(_("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 https://github.com/inkstitch/inkstitch/issues") + "\n") - errormsg(_("Include the error description and also (if possible) " - "the svg file.") + "\n") - errormsg(version.get_inkstitch_version() + "\n") - errormsg(exception) - sys.exit(1) - else: - sys.exit(0) + sys.exit(0) diff --git a/lib/api/stitch_plan.py b/lib/api/stitch_plan.py index c70efd982..5e9a57c15 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/elements/element.py b/lib/elements/element.py index 43cbc8a2d..963653af0 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) - stitch_groups = self.to_stitch_groups(last_stitch_group) - apply_patterns(stitch_groups, self.node) + if not stitch_groups: + self.validate() - 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 + stitch_groups = self.to_stitch_groups(last_stitch_group) + apply_patterns(stitch_groups, self.node) - self._save_cached_stitch_groups(stitch_groups, previous_stitch) + 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) 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 b93d7ff52..c7c3c640b 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) + 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() + 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/exceptions.py b/lib/exceptions.py index a9820ac30..3a6b456c6 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/params.py b/lib/extensions/params.py index 540cc7bb4..1ba144b24 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/gui/warnings.py b/lib/gui/warnings.py index 48788652a..eda1ca2e7 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/stitches/meander_fill.py b/lib/stitches/meander_fill.py index f61066062..c1308bf4e 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)