界面改用「精密仪器工作台」布局:顶栏压缩、视图切布局、标定与回放面板不再裁切

- What: 品牌条 40→28px 并加 实时监测/历史回放/标定 导航;导航改为切换主界面三套布局(实时=2D图像+曲线、回放=自动全程浏览、标定=标定面板 880px+曲线,2D 图像整体缩小);传感器选择(传感器 / 传感器配置 两个带 tooltip 的下拉 + 产品徽标)移到状态轨最左侧,记录模式移入设置区;新增 RecordStatusChip(记录中时长、丢帧、写入异常)与「同一时刻只有一个实心按钮」(新增 dangerOutline 描边危险样式);三轴读数移到曲线上方(16pt 等宽)、只保留最下方时间轴、网格淡化、全程浏览条可拖动;三轴配色统一为蓝/橙/绿并把语义色对比度审计扩展到 toolbar_bg;箭头页脚显示放大倍率与有效区域,深度模式才显示色条与范围;左侧面板改名「2D 图像」且固定显示图像宽高比;标定面板加纵向滚动区、九按钮分两行、六列表头 ResizeToContents;PlaybackDialog 补上从未调用的 retranslate;main() 在事件循环结束后 flush 日志并 os._exit,避免设备 SDK 原生线程把进程挂在解释器收尾。
- Why: 评审提出的 7 点:顶部占高与装饰过多、记录状态不突出、配置切换看不懂且藏得太深、切视图应换主界面布局、标定与回放面板文字被裁、关窗后命令行不退出、2D 图像无固定宽高比且名字不符。
- Verification: ui_check.py(430 项,新增 top area / instrument panels / clean shutdown 段,含中英×1280/1440 的轨不挤压检查)、test_ui_smoke.py(57 项,新增视图切换、标定不裁切、2D 宽高比、回放按钮文字、传感器选择器等用例)、drive_shared.py(146 项)、locale_studio.py(261 键齐全、zh==en、prefs 对齐)、reproduce.py、ui_snapshot.py 四张复核图。
- Not in this change: 录制文件格式、CSV/MP4 写入路径与记录通道数据流不变;三维面板仍是占位符;回放对话框(含离线深度重算)本身只修了按钮文字。
This commit is contained in:
ykcheungab
2026-09-13 18:54:31 +08:00
parent 0f4552cd4e
commit 35e86ec1f7
26 changed files with 2790 additions and 304 deletions
+30 -1
View File
@@ -327,7 +327,36 @@ outputs/recordings/<UTC 时间戳>_session<NNNN>/
---
## 9. 尚未完成(第三阶段及以后)
## 9. 界面上的记录状态条
录制过程对操作员可见的部分,集中在顶栏状态行右侧的一个记录状态条
(`apps/studio/src/orisys_studio/ui/record_status.py`)。它回答按钮本身答不了的四个问题:
| 显示 | 判据 | 颜色令牌 |
|---|---|---|
| `尚未记录` | 本会话从未开始记录(`recording.is_recording` 与 `last_report` 都为空) | `status_idle` |
| `记录中 mm:ss.d` | `recording.is_recording`;时长取自 `recorder.stats().duration_s` | `status_running` |
| `正在写入…` | `recording.report_pending`:写入线程还没关完文件(**不**当成已完成) | `status_connecting` |
| `上次记录 N 帧` | 已拿到终态报告且 `failed`/`write_errors` 为 0 | `status_idle` |
| `写入异常(N)` | `stats.failed` 或 `stats.write_errors > 0` | `status_error` |
丢帧不是单独一行,而是追加在状态后面(`丢帧 N`),并把整条状态提到 `warning` 色;
N 是 `dropped_data + dropped_video + dropped_gaps + unrecorded_gaps` —— 只报前两个
队列溢出计数会低估真实损失。tooltip 里分项列出帧数、分段、时长、四类丢帧、写入错误、
最近一次错误与输出目录,因此状态条本身可以保持一行。
读数入口是 `StudioMainWindow._record_stats()`:它在 UI 线程上返回
`(state, stats, directory)`,是**唯一**读记录通道的地方。`ExperimentRecorder.stats()`
返回加锁快照,不会被写入线程阻塞;`tools/ui_snapshot.py --scene recording` 就是替换这
一个接缝来渲染「记录中」画面,既不写文件也不开设备。
刷新时机:`_on_run_tick`(16 ms 定时器)与每次状态跃迁(开始/停止记录、拿到终态报告、
阶段变化)。控件只在文本或颜色令牌真的变化时才 `setText`/`setStyleSheet`,因此 60 Hz
下不会反复重排顶栏。
---
## 10. 尚未完成(第三阶段及以后)
- PyOpenGL 三维显示(`mesh3d_panel.py` 目前仍是占位符,不是已有实现)。
- CSV 的「导出选中时间段」(整段导出已随录制落盘;区间导出待做)。
+79
View File
@@ -0,0 +1,79 @@
# Studio 外壳:视图与关闭流程
这份文档记录主窗口的两件"外壳"行为:三个顶层视图如何切换面板,以及窗口关闭后进程
如何回到命令行。两者都不属于记录通道,但都会直接影响可用性。
---
## 1. 三个顶层视图 = 同一窗口的三套布局
品牌轨上的三个导航项不打开新窗口,而是切换主窗口的布局
(`StudioMainWindow._apply_view_preset`,预设在 `VIEW_PRESETS` / `VIEW_SPLITTER_SIZES`):
| 视图 | 面板 | 曲线模式 | 分割比例(3D, 2D, 曲线, 标定) |
|---|---|---|---|
| 实时监测 `live` | 2D 图像 + 力曲线(+ 手指三维) | 跟随实时 | `0, 360, 1080, 0` |
| 历史回放 `replay` | 2D 图像 + 力曲线 | 进入全程浏览 | `0, 320, 1120, 0` |
| 标定 `calibration` | 标定面板 + 力曲线 | 不变 | `0, 0, 560, 880` |
| 自由 `custom`(启动默认) | 由视图菜单决定 | 不变 | 用户在分割条上摆的位置 |
- **为什么标定视图给标定面板 880 px**:六列诊断表头加九个动作按钮,在 300–400 px 的
三栏布局里必然被挤压(原来的症状就是按钮文字被切成「排 除」「设为验证点」)。面板自身
还有纵向滚动区,窗口再矮也够得到「应用到标定」。
- **为什么导航不再自动打开回放对话框**:回放对话框(视频 + 离线深度重算 + 同一游标)仍
从「文件」菜单打开;导航的职责是把"看历史"这件事在主界面里摆好——全程浏览 + 2D 图像,
立刻可以拖时间轴。
- **`custom` 不点亮任何导航项**:手工摆出来的布局不属于三个视图中的任何一个,点亮一个
会误报状态。
- **`视图 → 标定面板` 与「标定」导航是同一个开关**:勾上进入标定视图,取消回到实时视图。
不允许出现"面板已隐藏但导航还亮着"。
回放对话框的三颗按钮曾经完全没有文字——`PlaybackDialog.retranslate()` 写好了却从没被
调用过。现在它在 `__init__` 末尾调用一次(`test_the_playback_dialog_labels_its_buttons`
守住这一点)。
---
## 2. 关闭流程:窗口关掉后进程必须回到命令行
关闭顺序(`StudioMainWindow.closeEvent` → `main()`):
1. `closeEvent`:
- 先摘掉记录通道的 sink,避免正在收尾的采集线程继续持有窗口;
- `_stop_run(reason="user_stop", wait=True)`:阻塞地释放设备;
- `_stop_recording(wait=True)`:写出 `session.json` 并关闭 MP4/CSV;
- `_persist_prefs()`:保存窗口几何、分割比例、视图与各项开关。
2. `app.exec_()` 返回后,`main()` **立刻结束进程**:
```python
code = int(app.exec_())
logging.shutdown()
for stream in (sys.stdout, sys.stderr):
try:
stream.flush()
except Exception:
pass
os._exit(code)
```
**为什么用 `os._exit`**:相机 SDK 与 OpenCV 各自持有原生线程与句柄,正常返回会在它们之上
再跑一遍解释器收尾(模块销毁、DLL 卸载),进程就卡在那里——症状正是"命令行不会完全退出"。
所有需要落盘的东西在 `closeEvent` 与 `logging.shutdown()` 里已经写完,这里跳过的只是
"已经没有东西可写的库"的析构。
`tools/ui_check.py` 的 `clean shutdown` 段在子进程里跑真实的 `main()`,用定时器关掉窗口
并断言进程在 45 秒内以 0 返回(实测约 2 秒),所以这条路径有回归保护。
---
## 3. 复核方式
```powershell
$py = "D:\orisys_ws\orisys-app\.venv-build\Scripts\python.exe"
$env:PYTHONPATH = "D:\orisys_ws\orisys-app\shared\src;D:\orisys_ws\orisys-app\apps\studio\src"
cd D:\orisys_ws\orisys-app\apps\studio
& $py tools/ui_check.py # 含 top area / instrument panels / clean shutdown
& $py tools/ui_snapshot.py --demo --preset calibration --out outputs/_snap_calibration.png
& $py tools/ui_snapshot.py --demo --scene recording --out outputs/_snap_recording.png
```
+103 -7
View File
@@ -25,6 +25,23 @@ orisys-app/shared/src/orisys_app_shared/theme/colors_light.json
| `#D8FF00` | 柠檬绿 | `flow_ramp` 第 5 档(原值保留)、`plot_fy`(深色) |
| `#D5D5D5` | 应用画布 | **未采用**,改为 `#E1E6EF`(见 §3.3) |
### 1.1 三轴分类色(蓝 / 橙 / 绿)
界面改版把三个力通道统一成"蓝 = 法向、橙 = 切向 X、绿 = 切向 Y"。它们是
**分类色**(三条曲线各自一个身份),与 §4 的箭头色带(顺序色带)是两件事,因此落在
`plot_fn` / `plot_fx` / `plot_fy` 三个令牌上。这三个令牌同时被实时曲线、录制回放、
标定面板与标定点详情对话框引用,换值即全局统一。
| 主题 | `plot_fn`(法向,蓝) | `plot_fx`(切向 X,橙) | `plot_fy`(切向 Y,绿) |
|---|---|---|---|
| 深色 | `#6FA8FF` | `#FF9A4D` | `#7CE0A0` |
| 浅色 | `#003893`(品牌藏青,见 §3.1) | `#B45309` | `#2E7D32` |
浅色主题的 FN 保持品牌藏青 `#003893`(与 `accent` 同值),这样品牌蓝在三轴里仍是
"主通道";橙色与绿色在浅底上取深色相,才够对比度。深色主题的霓虹洋红/柠檬绿被替换,
原因是它们是**色带**用色:作为分类色时洋红与橙的色相差偏小,而柠檬绿在浅底上不可用
(§2.1)。替换后三个通道在灰度打印下也靠亮度分开,不再依赖单一色相。
---
## 2. 实测对比度(WCAG)
@@ -55,21 +72,26 @@ orisys-app/shared/src/orisys_app_shared/theme/colors_light.json
### 2.2 落地后
浅色主题(节选):白字 on `accent` 10.57,白字 on `accent_hover` 5.52,
`plot_fn/fx/fy` 对 `plot_bg` 分别 10.11 / 6.00 / 4.89,
`plot_fn/fx/fy` 对 `plot_bg` 分别 10.11 / 4.80 / 4.90,
`brand_bar_fg` on `brand_bar_bg` 12.94,`input_border` 对四种底均 ≥3.08。
深色主题(节选):白字 on `accent` 5.52,`accent` 对 `panel_bg` 3.09(贴线但仍达标),
`plot_fn/fx/fy` 分别 7.61 / 6.16 / 15.73,`focus_ring` 对 `input_bg` 6.51。
`plot_fn/fx/fy` 对 `plot_bg` 分别 7.53 / 8.62 / 11.25,`focus_ring` 对 `input_bg` 6.51。
语义色(`danger` / `warning` / `success` / `status_*`)额外对 `toolbar_bg` 审计:它们会
出现在顶栏状态行上(记录状态、数据有效性、阶段指示),实测浅色 4.65–6.66、深色 5.60–9.13,
全部 ≥4.5。在这一步之前它们只在 `panel_bg` / `card_bg` 上被检查,顶栏是一个未经审计的底色。
三通道可分辨性(色相差 / 相对亮度比):
| 主题 | FN vs FX | FN vs FY | FX vs FY |
|---|---|---|---|
| 浅色 | 97.0° / 1.68 | 135.5° / 2.07 | 127.5° / 1.23 |
| 深色 | 96.7° / 1.24 | 154.7° / 2.07 | 108.7° / 2.55 |
| 浅色 | 168.8° / 2.10 | 94.1° / 2.06 | 97.1° / 1.02 |
| 深色 | 169.7° / 1.14 | 74.7° / 1.49 | 115.6° / 1.31 |
规则是"色相差 ≥90° **或** 相对亮度比 ≥1.2",两主题均满足。颜色在任何情况下都不是
唯一编码:每个曲线有文字标签,每张数值卡有同色通道徽标。
规则是"色相差 ≥90° **或** 相对亮度比 ≥1.2",两主题均满足(深色 FN vs FY 靠 1.49 的亮度比
过关,浅色 FX vs FY 靠 97.1° 的色相差过关)。颜色在任何情况下都不是唯一编码:每个曲线有
文字标签,每张数值卡有同色通道徽标与通道名。
---
@@ -148,7 +170,76 @@ flow_ramp = 画布色, #6E4AD0, #FA02F3, #FF8A6B, #D8FF00
---
## 6. 复核方式
## 6. 顶栏:一件仪器只有一条读数线
顶部由两条细轨组成,合计在 1440×880 下不超过 78 px(`ui_check.py` 的 `top area` 段与
`tests/test_ui_smoke.py::test_top_area_stays_compact` 都断言这一点):
| 轨 | 高度 | 承载 |
|---|---|---|
| 品牌轨(`QFrame#brandBar`) | 28 px | logo + 字标 + 三个顶层视图(实时监测 / 历史回放 / 标定)+ 右侧产品名 |
| 状态轨(`ToolbarWidget` 的 rail) | ~30 px | **传感器 / 传感器配置**、阶段、采集来源、标定状态、数据有效性、记录状态、记录/运行/重置/设置 |
| 设置区(默认收起) | 0 / ~40 px | 来源、摄像机编号、视频文件、单位、**记录模式** |
三点规则:
1. **传感器选择在状态轨最左侧,且两个下拉都带标签**。`box / mini / finger` 是产品
(传感器)类型,它决定后面可选的配置文件;原来只有一个「配置」下拉,看不出在切什么。
现在写成「传感器」+「传感器配置」两个带 tooltip 的下拉,产品型号另有一个徽标
(`FINGER` 不得被截断成 `INGE`,所以徽标只有最小宽度)。
2. **导航按钮在品牌底上取品牌前景色**。`navButton` 用 `brand_bar_fg_muted`,当前视图用
`brand_bar_fg` + `navActive="true"`;两者都在 §2 的品牌底对比度审计里(7.67 / 12.94),
不新增色值。
3. **状态轨上的语义色一律算在 `toolbar_bg` 上审计**。记录状态(`status_idle` /
`status_running` / `status_connecting` / `status_error`)、丢帧提示(`warning`)与数据
有效性(`success` / `warning`)此前只检查 `panel_bg`,而它们实际画在顶栏底色上。
4. **同一时刻只有一个实心按钮**(`ToolbarWidget._refresh_emphasis`):
| 状态 | 实心 | 描边 |
|---|---|---|
| 空闲 | `运行`(`primaryButton`) | — |
| 连接中 | `取消连接`(`dangerButton`) | — |
| 采集中 | `开始记录`(`primaryButton`) | `停止`(`dangerOutline`) |
| 记录中 | `停止记录`(`dangerButton`) | `停止`(`dangerOutline`) |
`dangerOutline` 是新增样式:透明底 + `danger` 文字与描边,hover/pressed 才填充
`danger_fill`;`:checked`(即"采集正在跑")仍保持描边——否则一圈红边会在会话开始后
悄悄变成整块红色,那正是这次要修掉的观感。
原反馈里"提高停止按钮优先级"的诉求没有丢:停止始终是红色、始终一键可达,只是不再
与当前主操作争夺同一时刻的唯一实心位置。
5. **状态轨上的事实不得被压缩**(`ui_check.rail_squeezed`)。中文、英文、1280 与 1440
四种组合下,轨上的按钮与下拉都必须显示完整文本;只有带 tooltip 的 `ShrinkableLabel`
允许在窗口极窄时被裁切。这也是"记录模式"下拉被挪进设置区的原因:英文的
`Start recording` + `Sensor config` 与它挤在同一行时,第一个被压缩的就是按钮文字。
---
## 7. 三个顶层视图 = 同一窗口的三套布局
导航不再打开对话框,而是切换主窗口的布局(`StudioMainWindow._apply_view_preset`):
| 视图 | 面板 | 曲线模式 | 分割比例(3D, 2D, 曲线, 标定) |
|---|---|---|---|
| 实时监测 | 2D 图像 + 力曲线(+ 手指三维) | 跟随实时 | `0, 360, 1080, 0` |
| 历史回放 | 2D 图像 + 力曲线 | 进入全程浏览 | `0, 320, 1120, 0` |
| 标定 | 标定面板 + 力曲线 | 不变 | `0, 0, 560, 880` |
| 自由(`custom`,启动默认) | 由视图菜单决定 | 不变 | 用户在分割条上摆的位置 |
- 标定视图给标定面板 880 px:六列诊断表头与九个动作按钮不再被挤压;标定面板自身还有
纵向滚动区,窗口再矮也够得到「应用到设备」。
- 标定面板列宽改为 `ResizeToContents`:诊断列头宁可让表格横向滚动,也不截成
「FN 均」这种看不懂的缩写。
- 2D 图像区域固定为所显示图像的宽高比(`AspectImageLabel`)。横向分割器**不会**向子控件
询问 `heightForWidth`,所以宽高比由 `ArrowPanel.resizeEvent` 主动施加;box 传感器的
画面旋转 90° 显示,因此其显示宽高比是 9:16 而不是 16:9。
- `视图 → 标定面板` 与「标定」导航是同一个开关:勾上进入标定视图,取消回到实时视图,
不会留下"面板已隐藏但导航还亮着"的矛盾状态。
---
## 8. 复核方式
```powershell
$py = "D:\orisys_ws\orisys-app\.venv-build\Scripts\python.exe"
@@ -158,4 +249,9 @@ cd D:\orisys_ws\orisys-app\apps\studio
& $py tools/ui_check.py # 含 palette 规则段落
& $py tools/ui_snapshot.py --theme dark --lang zh_CN --demo --out outputs/_snap_dark.png
& $py tools/ui_snapshot.py --theme light --lang en_US --sensor finger --panels cal,3d --size 1600x900 --demo --out outputs/_snap_light.png
& $py tools/ui_snapshot.py --theme dark --lang zh_CN --demo --scene recording --out outputs/_snap_recording.png
& $py tools/ui_snapshot.py --theme dark --lang zh_CN --demo --preset calibration --out outputs/_snap_calibration.png
```
视图切换与关闭流程另见 `apps/studio/docs/shell-lifecycle.md`。