From 913c2700d1486284dba0583ae1b280b1aa237570 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Wed, 29 Jan 2025 12:04:07 -0500 Subject: Sew Stack first steps (#3133) * handle more recursive cases * scaffolding for stitch layers * scaffolding for SewStack * always use DotDict when parsing json params * add DefaultDotDict + DotDict fixes * first working SewStack (no UI yet) * ignore inkstitch_debug.log and .svg * refactor * early WIP: property grid display temporarily in stitch plan preview * start of sew stack editor extension * add layer properties panel and splitter * spacing and better icon * handle checkbox * add layer action buttons * show selected property help text in an HtmlWindow * rename * rephrase help text for tolerance * refactor into separate file * simplify structure * better property type handling * add randomization button * add random seed re-roll button * simulator preview * update preview in a few more cases * always DotDict * avoid ridiculously slow simulations * preview selected layer or all layers * edit multiple objects and save only modified properties into the SVG * better preview handling * add reverse and jitter * add stitch path jitter * fix types * fix random shuffle button * fixes * fix repeats * type hinting to please pycharm * show layer description * avoid exception in properties with multiple values * fix typing * fix new layer * draw a box around property grid and help box * confirm before closing * rename properties and fix seed * fix close/cancel logic * add buttons to undo changes and reset to default value * set not modified if default is original setting * fix invisible icon * more space for properties * fix random properties * better regulation of simulator rendering speed * Fixed timer being passed a float * fix get_json_param() default handling * fix tests * add checkbox for sew stack only * fix property help * adjustable stitch layer editor help box size, with persistence * repeat exact stitches * "fix" style * adjust for new next_element stuff --------- Co-authored-by: CapellanCitizen --- lib/gui/simulator/control_panel.py | 6 ++++- lib/gui/simulator/drawing_panel.py | 42 ++++++++++++++++++----------- lib/gui/simulator/split_simulator_window.py | 7 +++++ lib/gui/windows.py | 41 ++++++++++++++++++++++++++++ 4 files changed, 80 insertions(+), 16 deletions(-) create mode 100644 lib/gui/windows.py (limited to 'lib/gui') diff --git a/lib/gui/simulator/control_panel.py b/lib/gui/simulator/control_panel.py index 9226d5de..99d1f92b 100644 --- a/lib/gui/simulator/control_panel.py +++ b/lib/gui/simulator/control_panel.py @@ -208,7 +208,11 @@ class ControlPanel(wx.Panel): self.set_speed(global_settings['simulator_speed']) return if self.target_duration: - self.set_speed(int(self.num_stitches / float(self.target_duration))) + stitches_per_second = round(self.num_stitches / float(self.target_duration)) + if stitches_per_second < 10: + # otherwise it just looks weirdly slow + stitches_per_second = 10 + self.set_speed(stitches_per_second) else: self.set_speed(self.target_stitches_per_second) diff --git a/lib/gui/simulator/drawing_panel.py b/lib/gui/simulator/drawing_panel.py index ced11f00..1587ecfa 100644 --- a/lib/gui/simulator/drawing_panel.py +++ b/lib/gui/simulator/drawing_panel.py @@ -52,8 +52,9 @@ class DrawingPanel(wx.Panel): self.SetDoubleBuffered(True) self.animating = False + self.timer = wx.Timer(self) + self.last_frame_start = 0 self.target_frame_period = 1.0 / self.TARGET_FPS - self.last_frame_duration = 0 self.direction = 1 self.current_stitch = 0 self.black_pen = wx.Pen((128, 128, 128)) @@ -72,6 +73,7 @@ class DrawingPanel(wx.Panel): self.Bind(wx.EVT_LEFT_DOWN, self.on_left_mouse_button_down) self.Bind(wx.EVT_MOUSEWHEEL, self.on_mouse_wheel) self.Bind(wx.EVT_SIZE, self.on_resize) + self.Bind(wx.EVT_TIMER, self.animate) # wait for layouts so that panel size is set if self.stitch_plan: @@ -99,20 +101,30 @@ class DrawingPanel(wx.Panel): elif self.direction == 1 and self.current_stitch < self.num_stitches: self.go() - def animate(self): + def animate(self, event=None): if not self.animating: return - frame_time = max(self.target_frame_period, self.last_frame_duration) - - # No sense in rendering more frames per second than our desired stitches - # per second. - frame_time = max(frame_time, 1.0 / self.speed) + # Each frame, we need to advance forward some number of stitches to + # match the speed setting. The tricky thing is that with bigger + # designs, it may take a long time to render a frame. That might + # mean that we'll fall behind. Even if we set our Timer to 30 FPS, + # we may only actually manage to render 20 FPS or fewer, and the + # duration of each frame may vary. + # + # To deal with that, we'll figure out how many stitches to advance + # based on how long it took to render the last frame. We'll always + # be behind by one frame, but it should work out fine. - stitch_increment = int(self.speed * frame_time) + now = time.time() + if self.last_frame_start: + frame_time = now - self.last_frame_start + else: + frame_time = self.target_frame_period + self.last_frame_start = now + stitch_increment = self.speed * frame_time self.set_current_stitch(self.current_stitch + self.direction * stitch_increment) - wx.CallLater(int(1000 * frame_time), self.animate) def OnPaint(self, e): dc = wx.PaintDC(self) @@ -167,7 +179,6 @@ class DrawingPanel(wx.Panel): stitch = 0 last_stitch = None - start = time.time() for pen, stitches, jumps in zip(self.pens, self.stitch_blocks, self.jumps): canvas.SetPen(pen) if stitch + len(stitches) < self.current_stitch: @@ -177,13 +188,12 @@ class DrawingPanel(wx.Panel): self.draw_needle_penetration_points(canvas, pen, stitches) last_stitch = stitches[-1] else: - stitches = stitches[:self.current_stitch - stitch] + stitches = stitches[:int(self.current_stitch) - stitch] if len(stitches) > 1: self.draw_stitch_lines(canvas, pen, stitches, jumps) self.draw_needle_penetration_points(canvas, pen, stitches) last_stitch = stitches[-1] break - self.last_frame_duration = time.time() - start if last_stitch: self.draw_crosshair(last_stitch[0], last_stitch[1], canvas, transform) @@ -260,7 +270,6 @@ class DrawingPanel(wx.Panel): def load(self, stitch_plan): self.current_stitch = 1 self.direction = 1 - self.last_frame_duration = 0 self.minx, self.miny, self.maxx, self.maxy = stitch_plan.bounding_box self.width = self.maxx - self.minx self.height = self.maxy - self.miny @@ -326,6 +335,7 @@ class DrawingPanel(wx.Panel): def stop(self): self.animating = False + self.timer.Stop() self.control_panel.on_stop() def go(self): @@ -335,6 +345,8 @@ class DrawingPanel(wx.Panel): if not self.animating: try: self.animating = True + self.last_frame_start = 0 + self.timer.Start(int(self.target_frame_period * 1000)) self.animate() self.control_panel.on_start() except RuntimeError: @@ -412,8 +424,8 @@ class DrawingPanel(wx.Panel): def set_current_stitch(self, stitch): self.current_stitch = stitch self.clamp_current_stitch() - command = self.commands[self.current_stitch] - self.control_panel.on_current_stitch(self.current_stitch, command) + command = self.commands[int(self.current_stitch)] + self.control_panel.on_current_stitch(int(self.current_stitch), command) statusbar = self.GetTopLevelParent().statusbar statusbar.SetStatusText(_("Command: %s") % COMMAND_NAMES[command], 2) self.stop_if_at_end() diff --git a/lib/gui/simulator/split_simulator_window.py b/lib/gui/simulator/split_simulator_window.py index 72fd1143..e4b2803e 100644 --- a/lib/gui/simulator/split_simulator_window.py +++ b/lib/gui/simulator/split_simulator_window.py @@ -67,6 +67,13 @@ class SplitSimulatorWindow(wx.Frame): def cancel(self, event=None): if self.cancel_hook: self.cancel_hook() + try: + if not self.settings_panel.confirm_close(): + event.Veto() + return + except AttributeError: + pass + self.close(None) def close(self, event=None): diff --git a/lib/gui/windows.py b/lib/gui/windows.py new file mode 100644 index 00000000..42c34cc8 --- /dev/null +++ b/lib/gui/windows.py @@ -0,0 +1,41 @@ +import wx + + +class SimpleBox(wx.Panel): + """Draw a box around one window. + + Usage: + + window = SomeWindow(your_window_or_panel, wx.ID_ANY) + box = SimpleBox(your_window_or_panel, window) + some_sizer.Add(box, ...) + + """ + + def __init__(self, parent, window, *args, width=1, radius=2, **kwargs): + super().__init__(parent, wx.ID_ANY, *args, **kwargs) + + window.Reparent(self) + self.window = window + self.sizer = wx.BoxSizer(wx.VERTICAL) + self.sizer.Add(window, 1, wx.EXPAND | wx.ALL, 2) + self.SetSizer(self.sizer) + + self.width = width + self.radius = radius + + self.Bind(wx.EVT_ERASE_BACKGROUND, self.on_erase_background) + + def on_erase_background(self, event): + dc = event.GetDC() + if not dc: + dc = wx.ClientDC(self) + size = self.GetClientSize() + + if wx.SystemSettings().GetAppearance().IsDark(): + dc.SetPen(wx.Pen(wx.Colour(32, 32, 32), width=self.width)) + else: + dc.SetPen(wx.Pen(wx.Colour(128, 128, 128), width=self.width)) + + dc.SetBrush(wx.NullBrush) + dc.DrawRoundedRectangle(0, 0, size.x, size.y, self.radius) -- cgit v1.2.3