From 3f0f04abec4ef1d12c670bca866db76a5a7d4d6a Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Tue, 30 Apr 2024 14:21:32 -0400 Subject: [PATCH] simulator fixes (#2844) * fix slide and control panel rendering bugs * clear marker lists when clearing stitch plan * switch simulator back to wx * remove unused function * fix off-by-one error in color bar * avoid overlapping command symbols of different types * don't maximize simulator * adjust alignment * remove unused API server * bugfix * focus entire simulator panel * rename simulator/realistic preview -> simulator * experimental: background color picker * set pagecolor to background color by default * satisfy macos * toggle jumps on drawing canvas * clear frog family --------- Co-authored-by: Kaalleen --- lib/api/__init__.py | 6 -- lib/api/lang.py | 11 --- lib/api/page_specs.py | 36 -------- lib/api/server.py | 121 ------------------------ lib/api/simulator.py | 8 -- lib/api/stitch_plan.py | 29 ------ lib/extensions/lettering.py | 3 + lib/extensions/params.py | 8 +- lib/extensions/simulator.py | 32 +++++-- lib/extensions/tartan.py | 3 + lib/gui/__init__.py | 2 +- lib/gui/lettering.py | 3 +- lib/gui/simulator.py | 174 +++++++++++++++++++++++------------ lib/gui/tartan/main_panel.py | 3 +- lib/utils/svg_data.py | 10 ++ templates/simulator.xml | 2 +- 16 files changed, 166 insertions(+), 285 deletions(-) delete mode 100644 lib/api/__init__.py delete mode 100644 lib/api/lang.py delete mode 100644 lib/api/page_specs.py delete mode 100644 lib/api/server.py delete mode 100644 lib/api/simulator.py delete mode 100644 lib/api/stitch_plan.py create mode 100644 lib/utils/svg_data.py diff --git a/lib/api/__init__.py b/lib/api/__init__.py deleted file mode 100644 index 35e411a75..000000000 --- a/lib/api/__init__.py +++ /dev/null @@ -1,6 +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 .server import APIServer \ No newline at end of file diff --git a/lib/api/lang.py b/lib/api/lang.py deleted file mode 100644 index 73c190f44..000000000 --- a/lib/api/lang.py +++ /dev/null @@ -1,11 +0,0 @@ -import os - -from flask import Blueprint, jsonify - -languages = Blueprint('languages', __name__) - - -@languages.route('') -def get_lang(): - languages = dict(os.environ) - return jsonify(languages) diff --git a/lib/api/page_specs.py b/lib/api/page_specs.py deleted file mode 100644 index 8d3aee49f..000000000 --- a/lib/api/page_specs.py +++ /dev/null @@ -1,36 +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 - -page_specs = Blueprint('page_specs', __name__) - - -@page_specs.route('') -def get_page_specs(): - svg = g.extension.document.getroot() - width = svg.get('width', 0) - height = svg.get('height', 0) - pagecolor = "white" - deskcolor = "white" - bordercolor = "black" - showpageshadow = True - - namedview = svg.namedview - if namedview is not None: - pagecolor = namedview.get('pagecolor', pagecolor) - deskcolor = namedview.get('inkscape:deskcolor', deskcolor) - bordercolor = namedview.get('bordercolor', bordercolor) - showpageshadow = namedview.get('inkscape:showpageshadow', showpageshadow) - - page_specs = { - "width": width, - "height": height, - "pagecolor": pagecolor, - "deskcolor": deskcolor, - "bordercolor": bordercolor, - "showpageshadow": showpageshadow - } - return jsonify(page_specs) diff --git a/lib/api/server.py b/lib/api/server.py deleted file mode 100644 index 5625d77de..000000000 --- a/lib/api/server.py +++ /dev/null @@ -1,121 +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. - -import errno -import logging -import socket -import sys -import time -from threading import Thread -from contextlib import closing - -import requests -from flask import Flask, g -from werkzeug.serving import make_server - -from ..utils.json import InkStitchJSONProvider -from .simulator import simulator -from .stitch_plan import stitch_plan -from .page_specs import page_specs -from .lang import languages -# this for electron axios -from flask_cors import CORS - - -class APIServer(Thread): - def __init__(self, *args, **kwargs): - self.extension = args[0] - Thread.__init__(self, *args[1:], **kwargs) - self.daemon = True - self.app = None - self.host = None - self.port = None - self.ready = False - - self.__setup_app() - self.flask_server = None - self.server_thread = None - - def __setup_app(self): # noqa: C901 - # Disable warning about using a development server in a production environment - cli = sys.modules['flask.cli'] - cli.show_server_banner = lambda *x: None - - self.app = Flask(__name__) - CORS(self.app) - self.app.json = InkStitchJSONProvider(self.app) - - self.app.register_blueprint(simulator, url_prefix="/simulator") - self.app.register_blueprint(stitch_plan, url_prefix="/stitch_plan") - self.app.register_blueprint(page_specs, url_prefix="/page_specs") - self.app.register_blueprint(languages, url_prefix="/languages") - - @self.app.before_request - def store_extension(): - # make the InkstitchExtension object available to the view handling - # this request - g.extension = self.extension - - @self.app.route('/ping') - def ping(): - return "pong" - - def stop(self): - self.flask_server.shutdown() - self.server_thread.join() - - def disable_logging(self): - logging.getLogger('werkzeug').setLevel(logging.ERROR) - - # https://github.com/aluo-x/Learning_Neural_Acoustic_Fields/blob/master/train.py - # https://github.com/pytorch/pytorch/issues/71029 - def find_free_port(self): - with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s: - s.bind(('localhost', 0)) - return s.getsockname()[1] - - def run(self): - self.disable_logging() - - self.host = "127.0.0.1" - self.port = self.find_free_port() - self.flask_server = make_server(self.host, self.port, self.app) - self.server_thread = Thread(target=self.flask_server.serve_forever) - self.server_thread.start() - - def ready_checker(self): - """Wait until the server is started. - - Annoyingly, there's no way to get a callback to be run when the Flask - server starts. Instead, we'll have to poll. - """ - - while True: - if self.port: - try: - response = requests.get("http://%s:%s/ping" % (self.host, self.port)) - if response.status_code == 200: - break - except socket.error as e: - if e.errno == errno.ECONNREFUSED: - pass - else: - raise - - time.sleep(0.1) - - def start_server(self): - """Start the API server. - - returns: port (int) -- the port that the server is listening on - (on localhost) - """ - - checker = Thread(target=self.ready_checker) - checker.start() - self.start() - checker.join() - - return self.port diff --git a/lib/api/simulator.py b/lib/api/simulator.py deleted file mode 100644 index 26c0246c3..000000000 --- a/lib/api/simulator.py +++ /dev/null @@ -1,8 +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 - -simulator = Blueprint('simulator', __name__) diff --git a/lib/api/stitch_plan.py b/lib/api/stitch_plan.py deleted file mode 100644 index 0267a70a5..000000000 --- a/lib/api/stitch_plan.py +++ /dev/null @@ -1,29 +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 - -from ..exceptions import InkstitchException, format_uncaught_exception -from ..stitch_plan import stitch_groups_to_stitch_plan - -stitch_plan = Blueprint('stitch_plan', __name__) - - -@stitch_plan.route('') -def get_stitch_plan(): - if not g.extension.get_elements(): - return dict(colors=[], stitch_blocks=[], commands=[]) - - try: - metadata = g.extension.get_inkstitch_metadata() - collapse_len = metadata['collapse_len_mm'] - min_stitch_len = metadata['min_stitch_len_mm'] - stitch_groups = g.extension.elements_to_stitch_groups(g.extension.elements) - stitch_plan = stitch_groups_to_stitch_plan(stitch_groups, 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/extensions/lettering.py b/lib/extensions/lettering.py index 43ff424d6..768219026 100644 --- a/lib/extensions/lettering.py +++ b/lib/extensions/lettering.py @@ -14,6 +14,7 @@ from ..gui.simulator import SplitSimulatorWindow from ..i18n import _ from ..svg import get_correction_transform from ..svg.tags import INKSCAPE_LABEL, INKSTITCH_LETTERING, SVG_GROUP_TAG +from ..utils.svg_data import get_pagecolor from .commands import CommandsExtension @@ -59,6 +60,7 @@ class Lettering(CommandsExtension): def effect(self): metadata = self.get_inkstitch_metadata() + background_color = get_pagecolor(self.svg.namedview) app = wx.App() frame = SplitSimulatorWindow( title=_("Ink/Stitch Lettering"), @@ -66,6 +68,7 @@ class Lettering(CommandsExtension): group=self.get_or_create_group(), on_cancel=self.cancel, metadata=metadata, + background_color=background_color, target_duration=1 ) diff --git a/lib/extensions/params.py b/lib/extensions/params.py index ea19672f0..0a593fdce 100644 --- a/lib/extensions/params.py +++ b/lib/extensions/params.py @@ -15,7 +15,6 @@ from secrets import randbelow import wx from wx.lib.scrolledpanel import ScrolledPanel -from .base import InkstitchExtension from ..commands import is_command, is_command_symbol from ..elements import (Clone, EmbroideryElement, FillStitch, Polyline, SatinColumn, Stroke) @@ -28,7 +27,9 @@ from ..stitch_plan import stitch_groups_to_stitch_plan from ..svg.tags import SVG_POLYLINE_TAG from ..utils import get_resource_dir from ..utils.param import ParamOption +from ..utils.svg_data import get_pagecolor from ..utils.threading import ExitThread, check_stop_flag +from .base import InkstitchExtension def grouper(iterable_obj, count, fillvalue=None): @@ -473,10 +474,11 @@ class ParamsTab(ScrolledPanel): class SettingsPanel(wx.Panel): - def __init__(self, parent, tabs_factory=None, on_cancel=None, metadata=None, simulator=None): + def __init__(self, parent, tabs_factory=None, on_cancel=None, metadata=None, background_color='white', simulator=None): self.tabs_factory = tabs_factory self.cancel_hook = on_cancel self.metadata = metadata + self.background_color = background_color self.simulator = simulator self.parent = parent @@ -782,12 +784,14 @@ class Params(InkstitchExtension): try: app = wx.App() metadata = self.get_inkstitch_metadata() + background_color = get_pagecolor(self.svg.namedview) frame = SplitSimulatorWindow( title=_("Embroidery Params"), panel_class=SettingsPanel, tabs_factory=self.create_tabs, on_cancel=self.cancel, metadata=metadata, + background_color=background_color, target_duration=5 ) diff --git a/lib/extensions/simulator.py b/lib/extensions/simulator.py index 3b532bf29..57978b73c 100644 --- a/lib/extensions/simulator.py +++ b/lib/extensions/simulator.py @@ -3,9 +3,11 @@ # Copyright (c) 2010 Authors # Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. -from ..api import APIServer -from ..gui import open_url +import wx +from ..gui.simulator import SimulatorWindow +from ..stitch_plan import stitch_groups_to_stitch_plan +from ..utils.svg_data import get_pagecolor from .base import InkstitchExtension @@ -16,9 +18,23 @@ class Simulator(InkstitchExtension): def effect(self): if not self.get_elements(): return - api_server = APIServer(self) - port = api_server.start_server() - electron = open_url("/simulator", port) - electron.wait() - api_server.stop() - api_server.join() + + metadata = self.get_inkstitch_metadata() + collapse_len = metadata['collapse_len_mm'] + min_stitch_len = metadata['min_stitch_len_mm'] + stitch_groups = self.elements_to_stitch_groups(self.elements) + stitch_plan = stitch_groups_to_stitch_plan(stitch_groups, collapse_len=collapse_len, min_stitch_len=min_stitch_len) + background_color = get_pagecolor(self.svg.namedview) + + app = wx.App() + current_screen = wx.Display.GetFromPoint(wx.GetMousePosition()) + display = wx.Display(current_screen) + screen_rect = display.GetClientArea() + height = int(screen_rect[3] * 0.8) + simulator = SimulatorWindow(size=(0, height), background_color=background_color) + wx.CallLater(100, simulator.Centre) + app.SetTopWindow(simulator) + simulator.Show() + simulator.load(stitch_plan) + simulator.go() + app.MainLoop() diff --git a/lib/extensions/tartan.py b/lib/extensions/tartan.py index a9b34dfbe..7f8c582a4 100644 --- a/lib/extensions/tartan.py +++ b/lib/extensions/tartan.py @@ -13,6 +13,7 @@ from ..gui.simulator import SplitSimulatorWindow from ..gui.tartan import TartanMainPanel from ..i18n import _ from ..svg.tags import EMBROIDERABLE_TAGS, INKSTITCH_TARTAN, SVG_GROUP_TAG +from ..utils.svg_data import get_pagecolor from .base import InkstitchExtension @@ -58,6 +59,7 @@ class Tartan(InkstitchExtension): errormsg(_("To create a tartan pattern please select at least one element with a fill color.")) return metadata = self.get_inkstitch_metadata() + background_color = get_pagecolor(self.svg.namedview) app = wx.App() frame = SplitSimulatorWindow( @@ -66,6 +68,7 @@ class Tartan(InkstitchExtension): elements=list(self.elements), on_cancel=self.cancel, metadata=metadata, + background_color=background_color, target_duration=1 ) diff --git a/lib/gui/__init__.py b/lib/gui/__init__.py index 4343e4d1e..8766b5cc3 100644 --- a/lib/gui/__init__.py +++ b/lib/gui/__init__.py @@ -6,5 +6,5 @@ from .dialogs import confirm_dialog, info_dialog from .electron import open_url from .presets import PresetsPanel -from .simulator import PreviewRenderer, show_simulator +from .simulator import PreviewRenderer from .warnings import WarningPanel diff --git a/lib/gui/lettering.py b/lib/gui/lettering.py index edbebb208..57d8a85d7 100644 --- a/lib/gui/lettering.py +++ b/lib/gui/lettering.py @@ -25,12 +25,13 @@ from . import PresetsPanel, PreviewRenderer, info_dialog class LetteringPanel(wx.Panel): DEFAULT_FONT = "small_font" - def __init__(self, parent, simulator, group, on_cancel=None, metadata=None): + def __init__(self, parent, simulator, group, on_cancel=None, metadata=None, background_color='white'): self.parent = parent self.simulator = simulator self.group = group self.cancel_hook = on_cancel self.metadata = metadata or dict() + self.background_color = background_color super().__init__(parent, wx.ID_ANY) diff --git a/lib/gui/simulator.py b/lib/gui/simulator.py index 423802f8f..79f3d95da 100644 --- a/lib/gui/simulator.py +++ b/lib/gui/simulator.py @@ -8,14 +8,15 @@ import time from threading import Event, Thread import wx +from numpy import split from wx.lib.intctrl import IntCtrl from lib.debug import debug from lib.utils import get_resource_dir from lib.utils.settings import global_settings from lib.utils.threading import ExitThread + from ..i18n import _ -from ..stitch_plan import stitch_plan_from_file from ..svg import PIXELS_PER_MM # L10N command label at bottom of simulator window @@ -43,8 +44,8 @@ class ControlPanel(wx.Panel): wx.Panel.__init__(self, parent, *args, **kwargs) self.drawing_panel = None - self.num_stitches = 1 - self.current_stitch = 1 + self.num_stitches = 0 + self.current_stitch = 0 self.speed = 1 self.direction = 1 self._last_color_block_end = 0 @@ -98,12 +99,15 @@ class ControlPanel(wx.Panel): 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, size=((100, -1)), style=wx.TE_PROCESS_ENTER) + self.stitchBox.Clear() self.stitchBox.Bind(wx.EVT_LEFT_DOWN, self.on_stitch_box_focus) self.stitchBox.Bind(wx.EVT_SET_FOCUS, self.on_stitch_box_focus) 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.totalstitchText = wx.StaticText(self, -1, label="/ ________") + self.totalstitchText = wx.StaticText(self, -1, label="") + extent = self.totalstitchText.GetTextExtent("0000000") + self.totalstitchText.SetMinSize(extent) self.btnJump = wx.BitmapToggleButton(self, -1, style=self.button_style) self.btnJump.SetToolTip(_('Show jump stitches')) self.btnJump.SetBitmap(self.load_icon('jump')) @@ -120,6 +124,9 @@ class ControlPanel(wx.Panel): 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)) + self.btnBackgroundColor = wx.ColourPickerCtrl(self, -1, colour='white', size=((40, -1))) + self.btnBackgroundColor.SetToolTip(_("Change background color")) + self.btnBackgroundColor.Bind(wx.EVT_COLOURPICKER_CHANGED, self.on_update_background_color) if self.detach_callback: self.btnDetachSimulator = wx.BitmapButton(self, -1, style=self.button_style) self.btnDetachSimulator.SetToolTip(_('Detach/attach simulator window')) @@ -129,8 +136,10 @@ class ControlPanel(wx.Panel): # 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 | wx.Right, 10) - self.hbSizer1.Add(self.totalstitchText, 0, wx.ALIGN_CENTER | wx.LEFT, 10) + self.hbSizer1.Add(self.stitchBox, 0, wx.ALIGN_TOP | wx.TOP, 25) + self.hbSizer1.Add((1, 1), 0, wx.RIGHT, 10) + self.hbSizer1.Add(self.totalstitchText, 0, wx.ALIGN_TOP | wx.TOP, 25) + self.hbSizer1.Add((1, 1), 0, wx.RIGHT, 10) self.controls_sizer = wx.StaticBoxSizer(wx.StaticBox(self, wx.ID_ANY, _("Controls")), wx.HORIZONTAL) self.controls_inner_sizer = wx.BoxSizer(wx.HORIZONTAL) @@ -147,11 +156,12 @@ class ControlPanel(wx.Panel): 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.btnNpp, 0, 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_inner_sizer.Add(self.btnBackgroundColor, 0, wx.EXPAND | wx.ALL, 2) if self.detach_callback: self.show_inner_sizer.Add(self.btnDetachSimulator, 0, wx.ALL, 2) self.show_sizer.Add((1, 1), 1) @@ -226,7 +236,6 @@ class ControlPanel(wx.Panel): self.accel_table = wx.AcceleratorTable(self.accel_entries) self.SetAcceleratorTable(self.accel_table) - self.SetFocus() # wait for layouts so that panel size is set if self.stitch_plan: @@ -241,27 +250,33 @@ class ControlPanel(wx.Panel): # otherwise the slider and intctrl get mad num_stitches = 2 self.num_stitches = num_stitches + self.stitchBox.SetValue(1) self.stitchBox.SetMax(num_stitches) self.slider.SetMax(num_stitches) self.totalstitchText.SetLabel(f"/ { 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 clear(self): + self.stitches = [] + self._set_num_stitches(0) + self.slider.clear() + self.stitchBox.Clear() + self.totalstitchText.SetLabel("") def load(self, stitch_plan): + self.clear() self.stitches = [] self._set_num_stitches(stitch_plan.num_stitches) stitch_num = 0 + last_block_end = 1 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) + end = start + color_block.num_stitches - 1 + self.slider.add_color_section(color_block.color.rgb, last_block_end, end) + last_block_end = end for stitch_num, stitch in enumerate(color_block.stitches, start): if stitch.trim: @@ -280,6 +295,16 @@ class ControlPanel(wx.Panel): def on_marker_button(self, marker_type, event): self.slider.enable_marker_list(marker_type, event.GetEventObject().GetValue()) + if marker_type == 'jump': + self.drawing_panel.Refresh() + + def on_update_background_color(self, event): + self.set_background_color(event.Colour) + + def set_background_color(self, color): + self.btnBackgroundColor.SetColour(color) + self.drawing_panel.SetBackgroundColour(color) + self.drawing_panel.Refresh() def choose_speed(self): if self.target_duration: @@ -539,18 +564,18 @@ class DrawingPanel(wx.Panel): last_stitch = None start = time.time() - for pen, stitches in zip(self.pens, self.stitch_blocks): + for pen, stitches, jumps in zip(self.pens, self.stitch_blocks, self.jumps): canvas.SetPen(pen) if stitch + len(stitches) < self.current_stitch: stitch += len(stitches) if len(stitches) > 1: - canvas.StrokeLines(stitches) + self.draw_stitch_lines(canvas, pen, stitches, jumps) self.draw_needle_penetration_points(canvas, pen, stitches) last_stitch = stitches[-1] else: stitches = stitches[:self.current_stitch - stitch] if len(stitches) > 1: - canvas.StrokeLines(stitches) + self.draw_stitch_lines(canvas, pen, stitches, jumps) self.draw_needle_penetration_points(canvas, pen, stitches) last_stitch = stitches[-1] break @@ -607,6 +632,16 @@ class DrawingPanel(wx.Panel): canvas.EndLayer() + def draw_stitch_lines(self, canvas, pen, stitches, jumps): + render_jumps = self.control_panel.btnJump.GetValue() + if render_jumps: + canvas.StrokeLines(stitches) + else: + stitch_blocks = split(stitches, jumps) + for i, block in enumerate(stitch_blocks): + if len(block) > 1: + canvas.StrokeLines(block) + def draw_needle_penetration_points(self, canvas, pen, stitches): if self.control_panel.btnNpp.GetValue(): npp_pen = wx.Pen(pen.GetColour(), width=int(0.5 * PIXELS_PER_MM * self.PIXEL_DENSITY)) @@ -669,6 +704,7 @@ class DrawingPanel(wx.Panel): def parse_stitch_plan(self, stitch_plan): self.pens = [] self.stitch_blocks = [] + self.jumps = [] # There is no 0th stitch, so add a place-holder. self.commands = [None] @@ -676,6 +712,8 @@ class DrawingPanel(wx.Panel): for color_block in stitch_plan: pen = self.color_to_pen(color_block.color) stitch_block = [] + jumps = [] + stitch_index = 0 for stitch in color_block: # trim any whitespace on the left and top and scale to the @@ -687,6 +725,7 @@ class DrawingPanel(wx.Panel): self.commands.append(TRIM) elif stitch.jump: self.commands.append(JUMP) + jumps.append(stitch_index) elif stitch.stop: self.commands.append(STOP) elif stitch.color_change: @@ -698,10 +737,16 @@ class DrawingPanel(wx.Panel): self.pens.append(pen) self.stitch_blocks.append(stitch_block) stitch_block = [] + self.jumps.append(jumps) + jumps = [] + stitch_index = 0 + else: + stitch_index += 1 if stitch_block: self.pens.append(pen) self.stitch_blocks.append(stitch_block) + self.jumps.append(jumps) def set_speed(self, speed): self.speed = speed @@ -805,11 +850,12 @@ class DrawingPanel(wx.Panel): class MarkerList(list): - def __init__(self, icon_name, stitch_numbers=()): + def __init__(self, icon_name, offset=0, 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.offset = offset self.enabled = False self.extend(stitch_numbers) @@ -828,32 +874,32 @@ class ColorSection: class SimulatorSlider(wx.Panel): PROXY_EVENTS = (wx.EVT_SLIDER,) - def __init__(self, parent, id=wx.ID_ANY, minValue=0, maxValue=1, **kwargs): + def __init__(self, parent, id=wx.ID_ANY, minValue=1, maxValue=2, **kwargs): super().__init__(parent, id) kwargs['style'] = wx.SL_HORIZONTAL | wx.SL_VALUE_LABEL | wx.SL_TOP | wx.ALIGN_TOP - self._height = self.GetTextExtent("M").y * 4 + self._height = self.GetTextExtent("M").y * 6 self.SetMinSize((self._height, self._height)) self.marker_lists = { "trim": MarkerList("trim"), - "stop": MarkerList("stop"), - "jump": MarkerList("jump"), - "color_change": MarkerList("color_change"), + "jump": MarkerList("jump", 0.17), + "stop": MarkerList("stop", 0.34), + "color_change": MarkerList("color_change", 0.34), } self.marker_pen = wx.Pen(wx.Colour(0, 0, 0)) self.color_sections = [] self.margin = 15 self.tab_start = 0 - self.tab_width = 0.2 - self.tab_height = 0.2 - self.color_bar_start = 0.3 - self.color_bar_thickness = 0.25 + self.tab_width = 0.15 + self.tab_height = 0.15 + self.color_bar_start = 0.22 + self.color_bar_thickness = 0.17 self.marker_start = self.color_bar_start - self.marker_end = 0.75 - self.marker_icon_start = 0.75 - self.marker_icon_size = self._height // 4 + self.marker_end = 0.5 + self.marker_icon_start = 0.5 + self.marker_icon_size = self._height // 6 self._min = minValue self._max = maxValue @@ -884,6 +930,16 @@ class SimulatorSlider(wx.Panel): def GetValue(self): return self._value + def clear(self): + self.color_sections = [] + self._min = 1 + self._max = 2 + self._value = 0 + self._tab_rect = None + + for marker_list in self.marker_lists.values(): + marker_list.clear() + def add_color_section(self, color, start, end): self.color_sections.append(ColorSection(color, start, end)) @@ -912,6 +968,9 @@ class SimulatorSlider(wx.Panel): dc.Clear() gc = wx.GraphicsContext.Create(dc) + if self._value < self._min: + return + width, height = self.GetSize() min_value = self._min max_value = self._max @@ -952,11 +1011,11 @@ class SimulatorSlider(wx.Panel): x = _value_to_x(value) gc.StrokeLine( x, height * self.marker_start, - x, height * self.marker_end + x, height * (self.marker_end + marker_list.offset) ) gc.DrawBitmap( marker_list.icon, - x - self.marker_icon_size / 2, height * self.marker_icon_start, + x - self.marker_icon_size / 2, height * (self.marker_icon_start + marker_list.offset), self.marker_icon_size, self.marker_icon_size ) @@ -1011,7 +1070,7 @@ class SimulatorSlider(wx.Panel): class SimulatorPanel(wx.Panel): """""" - def __init__(self, parent, stitch_plan=None, target_duration=5, stitches_per_second=16, detach_callback=None): + def __init__(self, parent, stitch_plan=None, background_color='white', target_duration=5, stitches_per_second=16, detach_callback=None): """""" super().__init__(parent, style=wx.BORDER_SUNKEN) @@ -1022,6 +1081,7 @@ class SimulatorPanel(wx.Panel): detach_callback=detach_callback) self.dp = DrawingPanel(self, stitch_plan=stitch_plan, control_panel=self.cp) self.cp.set_drawing_panel(self.dp) + self.cp.set_background_color(wx.Colour(background_color)) vbSizer = wx.BoxSizer(wx.VERTICAL) vbSizer.Add(self.dp, 1, wx.EXPAND | wx.ALL, 2) @@ -1040,10 +1100,12 @@ class SimulatorPanel(wx.Panel): def clear(self): self.dp.clear() + self.cp.clear() class SimulatorWindow(wx.Frame): def __init__(self, panel=None, parent=None, **kwargs): + background_color = kwargs.pop('background_color', 'white') super().__init__(None, title=_("Embroidery Simulation"), **kwargs) self.SetWindowStyle(wx.FRAME_FLOAT_ON_PARENT | wx.DEFAULT_FRAME_STYLE) @@ -1062,8 +1124,8 @@ class SimulatorWindow(wx.Frame): self.panel.Show() else: self.is_child = False - self.simulator_panel = SimulatorPanel(self) - self.sizer.Add(self.simulator_panel, 1, wx.EXPAND) + self.panel = SimulatorPanel(self, background_color=background_color) + self.sizer.Add(self.panel, 1, wx.EXPAND) self.SetSizer(self.sizer) self.Layout() @@ -1072,6 +1134,8 @@ class SimulatorWindow(wx.Frame): if self.is_child: self.Bind(wx.EVT_CLOSE, self.on_close) + else: + self.Maximize() def detach_simulator_panel(self): self.sizer.Detach(self.panel) @@ -1079,6 +1143,12 @@ class SimulatorWindow(wx.Frame): def on_close(self, event): self.parent.attach_simulator() + def load(self, stitch_plan): + self.panel.load(stitch_plan) + + def go(self): + self.panel.go() + class SplitSimulatorWindow(wx.Frame): def __init__(self, panel_class, title, target_duration=None, **kwargs): @@ -1088,7 +1158,13 @@ class SplitSimulatorWindow(wx.Frame): self.detached_simulator_frame = None self.splitter = wx.SplitterWindow(self, style=wx.SP_LIVE_UPDATE) - self.simulator_panel = SimulatorPanel(self.splitter, target_duration=target_duration, detach_callback=self.toggle_detach_simulator) + background_color = kwargs.pop('background_color', 'white') + self.simulator_panel = SimulatorPanel( + self.splitter, + background_color=background_color, + target_duration=target_duration, + detach_callback=self.toggle_detach_simulator + ) self.settings_panel = panel_class(self.splitter, simulator=self.simulator_panel, **kwargs) self.splitter.SplitVertically(self.settings_panel, self.simulator_panel) @@ -1105,6 +1181,7 @@ class SplitSimulatorWindow(wx.Frame): self.SetMinSize(self.sizer.CalcMin()) + self.simulator_panel.SetFocus() self.Maximize() self.Show() wx.CallLater(100, self.set_sash_position) @@ -1146,7 +1223,7 @@ class SplitSimulatorWindow(wx.Frame): self.detached_simulator_frame = None self.Maximize() self.splitter.UpdateSize() - self.SetFocus() + self.simulator_panel.SetFocus() self.Raise() wx.CallLater(100, self.set_sash_position) global_settings['pop_out_simulator'] = False @@ -1228,26 +1305,3 @@ class PreviewRenderer(Thread): except: # noqa: E722 import traceback debug.log("unhandled exception in PreviewRenderer.render_stitch_plan(): " + traceback.format_exc()) - - -def show_simulator(stitch_plan): - app = wx.App() - current_screen = wx.Display.GetFromPoint(wx.GetMousePosition()) - display = wx.Display(current_screen) - screen_rect = display.GetClientArea() - - simulator_pos = (screen_rect[0], screen_rect[1]) - - # subtract 1 because otherwise the window becomes maximized on Linux - width = screen_rect[2] - 1 - height = screen_rect[3] - 1 - - frame = SimulatorWindow(pos=simulator_pos, size=(width, height), stitch_plan=stitch_plan) - app.SetTopWindow(frame) - frame.Show() - app.MainLoop() - - -if __name__ == "__main__": - stitch_plan = stitch_plan_from_file(sys.argv[1]) - show_simulator(stitch_plan) diff --git a/lib/gui/tartan/main_panel.py b/lib/gui/tartan/main_panel.py index 238c8901d..826381705 100644 --- a/lib/gui/tartan/main_panel.py +++ b/lib/gui/tartan/main_panel.py @@ -26,13 +26,14 @@ from . import CodePanel, CustomizePanel, EmbroideryPanel, HelpPanel class TartanMainPanel(wx.Panel): - def __init__(self, parent, simulator, elements, on_cancel=None, metadata=None, output_groups=inkex.Group()): + def __init__(self, parent, simulator, elements, on_cancel=None, metadata=None, background_color='white', output_groups=inkex.Group()): self.parent = parent self.simulator = simulator self.elements = elements self.cancel_hook = on_cancel self.palette = Palette() self.metadata = metadata or dict() + self.background_color = background_color self.output_groups = output_groups super().__init__(parent, wx.ID_ANY) diff --git a/lib/utils/svg_data.py b/lib/utils/svg_data.py new file mode 100644 index 000000000..c3ef672a6 --- /dev/null +++ b/lib/utils/svg_data.py @@ -0,0 +1,10 @@ +# Authors: see git history +# +# Copyright (c) 2024 Authors +# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. + +def get_pagecolor(namedview, default_color='white'): + pagecolor = default_color + if namedview is not None: + pagecolor = namedview.get('pagecolor', pagecolor) + return pagecolor diff --git a/templates/simulator.xml b/templates/simulator.xml index 813b09b5a..94568c3f4 100644 --- a/templates/simulator.xml +++ b/templates/simulator.xml @@ -1,6 +1,6 @@ - Simulator / Realistic Preview + Simulator org.{{ id_inkstitch }}.simulator simulator