speed up mock

This commit is contained in:
2026-03-13 00:39:06 +01:00
parent bec76d6796
commit 80dccdb38a
4 changed files with 95 additions and 75 deletions

View File

@@ -13,6 +13,12 @@ uv run -m frontend
## Navigation
- L: left press
- LL: left long press
- R: right press
- RL: right long press
- LB: long left and right press
```mermaid
stateDiagram-v2
[*] --> Main
@@ -28,34 +34,34 @@ stateDiagram-v2
state "Edit Step Value Entry<br>(morse input)" as ValueEntry
state "Edit Step Goal Time Entry<br>(morse input)" as GoalTime
Main --> Menu : both long press
Main --> RecipeSelection : right long press
Main --> Menu : LB
Main --> RecipeSelection : RL
Menu --> Main : left long press [back]
Menu --> BTPairing : right long press [BT Pair]
Menu --> Main : LL [back]
Menu --> BTPairing : RL [BT Pair]
BTPairing --> Menu : left long press [cancel]
BTPairing --> Menu : LL [cancel]
RecipeSelection --> Main : left long press
RecipeSelection --> DoRecipe : right long press [on recipe → run]
RecipeSelection --> EditRecipe : both long press [on recipe → edit]<br>or right long press [on + → add]
RecipeSelection --> Main : LL
RecipeSelection --> DoRecipe : RL [on recipe → run]
RecipeSelection --> EditRecipe : LB [on recipe → edit]<br>or RL [on + → add]
DoRecipe --> Main : left long press [back]<br>or right press [last step → done]
DoRecipe --> Main : LL [back]<br>or R [last step → done]
EditRecipe --> RecipeSelection : left long press [back]<br>or right long press [on save]
EditRecipe --> TypeSelect : right long press [on step]<br>or right long press [on + → add step]
EditRecipe --> ValueEntry : right long press [on name]
EditRecipe --> MoveMode : both long press [on step]
EditRecipe --> RecipeSelection : LL [back]<br>or RL [on save]
EditRecipe --> TypeSelect : RL [on step]<br>or RL [on + → add step]
EditRecipe --> ValueEntry : RL [on name]
EditRecipe --> MoveMode : LB [on step]
MoveMode --> EditRecipe : both long press [confirm]<br>or left long press [cancel]
MoveMode --> EditRecipe : LB [confirm]<br>or LL [cancel]
TypeSelect --> ValueEntry : right long press [type with value]
TypeSelect --> EditRecipe : right long press [type w/o value → saved]<br>or right long press [delete → deleted]<br>or left long press [back]
TypeSelect --> ValueEntry : RL [type with value]
TypeSelect --> EditRecipe : RL [type w/o value → saved]<br>or RL [delete → deleted]<br>or LL [back]
ValueEntry --> EditRecipe : left long press [save]<br>or left press [cancel]
ValueEntry --> GoalTime : left long press [WEIGH_WITH_TIMER]
ValueEntry --> EditRecipe : LL [save]<br>or L [cancel]
ValueEntry --> GoalTime : LL [WEIGH_WITH_TIMER]
GoalTime --> EditRecipe : left long press [save]<br>or left press [cancel]
GoalTime --> EditRecipe : LL [save]<br>or L [cancel]
```
## Tools

View File

@@ -12,6 +12,7 @@ from python_toolkit.gui.connect import ConnectFrame
from .config import DEFAULT_CALIB_WEIGHT, DEFAULT_CALIB, DISPLAY_TYPES, MOV_AVG_DEFAULTS
from .views import MainView
from .views.loading import FRAMES
class WeightApp(tk.Tk):
def __init__(self, weight_reader: SerialReader):

View File

@@ -1,41 +1,54 @@
from PIL import Image, ImageDraw
# SVG cubic bezier segments (absolute coordinates in 500x500 viewport)
# P path: starts at center, goes up and curves around the bump
_P_SEGMENTS = [
((248.462, 250.406), (248.125, 213.074), (246.529, 178.548), (246.529, 178.548)),
((246.529, 178.548), (250.153, 120.959), (302.781, 109.091), (316.648, 109.906)),
((316.648, 109.906), (384.748, 113.909), (403.0, 185.637), (372.292, 222.732)),
]
# J path: starts at bottom-left hook, ends at center
_J_SEGMENTS = [
((113.29, 351.138), (138.503, 404.984), (237.45, 410.427), (246.53, 320.295)),
((246.53, 320.295), (248.338, 302.355), (248.69, 275.888), (248.466, 249.666)),
]
class LoadingView:
"""Loading screen with animated JP logo. Circle matches CircleView."""
SETUP_FRAMES = 50
SETUP_FRAMES = 100
ANIMATION_FRAMES = 100 # ~1s at 50fps
HOLD_FRAMES = 100 # hold completed logo ~9s (total ~10s)
# SVG cubic bezier segments (absolute coordinates in 500x500 viewport)
# P path: starts at center, goes up and curves around the bump
_P_SEGMENTS = [
((248.462, 250.406), (248.125, 213.074), (246.529, 178.548), (246.529, 178.548)),
((246.529, 178.548), (250.153, 120.959), (302.781, 109.091), (316.648, 109.906)),
((316.648, 109.906), (384.748, 113.909), (403.0, 185.637), (372.292, 222.732)),
]
# J path: starts at bottom-left hook, ends at center
_J_SEGMENTS = [
((113.29, 351.138), (138.503, 404.984), (237.45, 410.427), (246.53, 320.295)),
((246.53, 320.295), (248.338, 302.355), (248.69, 275.888), (248.466, 249.666)),
]
def __init__(self, size, center, radius_offset=11):
self.size = size
self.center = center
self.target_radius = min(center) - radius_offset
im_size = (168, 144)
center = (168 // 2, 144 // 2)
TARGET_RADIUS = min(center) - 11 # 11px padding to match CircleView's radius_offset
SCALE = TARGET_RADIUS / 240.0
svg_cx, svg_cy = 252.565, 249.335
dx = center[0] - svg_cx * SCALE
dy = center[1] - svg_cy * SCALE
def __init__(self, size, center):
self.frame = 0
self.total_frames = self.SETUP_FRAMES + self.ANIMATION_FRAMES + self.HOLD_FRAMES
# Transform SVG coords → display coords
scale = self.target_radius / 240.0
svg_cx, svg_cy = 252.565, 249.335
dx = self.center[0] - svg_cx * scale
dy = self.center[1] - svg_cy * scale
# Pre-render all frames at init time
self._cached_frames = FRAMES
self.p_points = self._sample_path(self._P_SEGMENTS, scale, dx, dy)
self.j_points = self._sample_path(self._J_SEGMENTS, scale, dx, dy)
@staticmethod
def _build_frame_cache():
frames = []
blank = LoadingView._render(0.0)
for i in range(LoadingView.SETUP_FRAMES):
frames.append(blank)
for i in range(LoadingView.ANIMATION_FRAMES):
progress = LoadingView._ease_in_out(i / LoadingView.ANIMATION_FRAMES)
frames.append(LoadingView._render(progress))
done = LoadingView._render(1.0)
for i in range(LoadingView.HOLD_FRAMES):
frames.append(done)
return frames
@staticmethod
def _sample_path(segments, scale, dx, dy, samples_per_segment=30):
@@ -60,39 +73,39 @@ class LoadingView:
def get_frame(self):
"""Return (PIL Image, is_done) advancing the animation by one frame."""
if self.frame >= self.total_frames:
return self._render(1.0), True
return self._cached_frames[-1], True
if self.frame >= self.SETUP_FRAMES + self.ANIMATION_FRAMES:
progress = 1.0
elif self.frame < self.SETUP_FRAMES:
progress = 0.0
else:
progress = self._ease_in_out((self.frame - self.SETUP_FRAMES) / self.ANIMATION_FRAMES)
im = self._render(progress)
im = self._cached_frames[self.frame]
self.frame += 1
return im, False
def _render(self, progress):
im = Image.new('1', self.size, 'white')
@staticmethod
def _render(progress):
im = Image.new('1', LoadingView.im_size, 'white')
draw = ImageDraw.Draw(im)
# Circle outline — same as CircleView
draw.circle(self.center, self.target_radius, outline='black')
draw.circle(LoadingView.center, LoadingView.TARGET_RADIUS, outline='black')
# P path: reveal from start forward (center appears first, extends toward bump)
p_count = int(progress * len(self.p_points))
p_count = int(progress * len(P_POINTS))
if p_count > 1:
pts = self.p_points[:p_count]
pts = P_POINTS[:p_count]
for i in range(len(pts) - 1):
draw.line([pts[i], pts[i + 1]], fill='black', width=2)
# J path: reveal from end backward (center appears first, extends toward hook)
j_count = int(progress * len(self.j_points))
j_count = int(progress * len(J_POINTS))
if j_count > 1:
start = len(self.j_points) - j_count
pts = self.j_points[start:]
start = len(J_POINTS) - j_count
pts = J_POINTS[start:]
for i in range(len(pts) - 1):
draw.line([pts[i], pts[i + 1]], fill='black', width=2)
return im
P_POINTS = LoadingView._sample_path(_P_SEGMENTS, LoadingView.SCALE, LoadingView.dx, LoadingView.dy)
J_POINTS = LoadingView._sample_path(_J_SEGMENTS, LoadingView.SCALE, LoadingView.dx, LoadingView.dy)
FRAMES = LoadingView._build_frame_cache()

View File

@@ -1,8 +1,7 @@
import io
import tkinter as tk
from tkinter import Frame, Canvas, ttk, PhotoImage
from tkinter import Frame, Canvas, ttk
from PIL import ImageChops
from PIL import ImageChops, ImageTk
from ..config import DISPLAY_TYPES, DISPLAY_MODES
from .draw_utils import draw_clock, draw_bluetooth_icon
@@ -52,6 +51,7 @@ class MainView(tk.Frame, ButtonInterface):
self.buttons = ButtonsManager(self, self.im_size, self.center,
curr_view=self)
self.loading_view = LoadingView(self.im_size, self.center)
self._prev_frame_bytes = None
self.recipes_manager = RecipeManager()
self.current_steps = [None] * 3
self.tare_buffer = []
@@ -323,18 +323,18 @@ class MainView(tk.Frame, ButtonInterface):
ims.append(recipe_im)
self.canvas.delete("all")
# Combine images by logical_and
if ims:
combined_im = ims[0]
for im in ims[1:]:
combined_im = ImageChops.invert(ImageChops.logical_xor(combined_im, im))
# Convert PIL image to bytes
buffer = io.BytesIO()
combined_im.save(buffer, format='PNG')
buffer.seek(0)
# Skip redraw if frame hasn't changed
frame_bytes = combined_im.tobytes()
if frame_bytes == self._prev_frame_bytes:
return
self._prev_frame_bytes = frame_bytes
# Load into PhotoImage and display on canvas
self.photo = PhotoImage(data=buffer.getvalue())
self.canvas.delete("all")
self.photo = ImageTk.PhotoImage(combined_im)
self.canvas.create_image(0, 0, anchor="nw", image=self.photo)