Files
orisys-app/apps/studio/docs/ui-palette.md
T
ykcheungab 9b3c5fe46c 回放与实时统一为一套界面;修复 30 fps 设备被以 16.7 fps 消费的计时器缺陷 - What: 1) 删除 ui/replay_bar.py(进度条、录制目录选择、重算深度按钮、提示行)与回放条的全部接线:历史回放现在与实时监测同布局(分割比例同为 0,360,1080,0)、同一条状态轨、同一套曲线手势,唯一差别是来源槽——实时是摄像机,回放是「选择录制视频…」(toolbar.btn_replay_video,与文件菜单「打开视频…」同一条 _load_replay_video 路径:视频所在目录即录制目录,曲线自动配对同目录 forces.csv)。时间轴改为曲线游标本身:表头新增播放/暂停图标按钮(icons.play/pause,空格键同效)以 1× 推进(按墙钟计时,漏拍不变慢),到末尾自动停;拖动游标、框选、滚轮平移或拖总览条一律自动暂停(ForcePlotsPanel.pause_playback)。回放的降采样/跨版本提示移入来源摘要 tooltip,打开录制时另发一条 6 秒通知。修复视频文件名残留在实时视图的问题:_refresh_status_rail 只在 _view_preset == "replay" 时写视频名。2) 录制格式不变:raw_*.mp4 与 forces.csv/frames.csv/session.json 继续一起写。按「两者都要」新增 视图 → 由视频重算力值(act_recompute):默认曲线读录制的 CSV,打开后把录制视频重新跑一遍视觉管线(进 SessionStore、2D 面板交给管线绘制、只能顺序前进,播放按钮即开始/暂停,关闭即挂回 CSV),启动路径唯一(set_playing → _on_chart_play_toggled → _start_run),并让文件源的时间轴改为视频自身时间(mono_t = 帧号 × 1/fps),否则重算结果会落在与 CSV 不同的横轴上。回放 2D 面板不再提供深度视图(REPLAY_VIEW_MODES=("recorded",)),离线深度重算引擎保留但无界面入口。3) 采集只有 16.67 fps 的根因是上一轮引入的等待:Windows 默认计时器分辨率 15.6 ms,time.sleep(0.002) 实测睡 15.5 ms,重复帧等两拍即跨过 30 fps 的 33 ms 周期,30 fps 设备被以 ~16.7 fps 消费并隔帧丢弃(配置本身正确:mini/sensor.json 写 camera.fps=30;离线实测整套管线 15.2 ms/帧≈66 fps,余量充足)。新增 _high_resolution_timer() 在会话期间把分辨率提到 1 ms(结束还原),并把重复帧等待改为 min(距下一帧时间, 半个周期)、到期只 sleep(0)。 - Why: 用户评审 3 点——回放界面与实时不一致且拖拽条效果一般、不想为回放录制深度信息、想同时保留视频与 CSV 记录、以及采集帧率只有 16.67 fps(应为 30)。 - Verification: ui_check.py 562 项 0 失败(新增两视图同布局/无 bar、来源槽随视图切换且无残留、播放暂停与手动手势暂停、重算模式切换与视频源、回放提示在 tooltip);test_ui_smoke.py 73 项 0 失败(新增视图统一、传输控制、重算模式);drive_shared.py 150 项 0 失败(新增 shared/tests/test_worker_frame_rate.py::test_a_file_source_runs_on_the_video_s_own_timeline,并把 30 fps 用例改成必须落在 27–31.5 fps;同文件实测无 1 ms 计时器 21.6 fps、有则 30.0 fps,同一帧返回 3 次时 11317:39);locale_studio.py 266 键齐全、zh==en;reproduce.py 退出 0;ui_snapshot.py 三张复核图(统一后的回放、实时、选区中英)。 - Not in this change: 录制文件格式与 CSV 列序不变;离线深度重算引擎(ReplayController.request_depth / DepthRecomputer)保留并有独立测试,但界面已无入口,可下一步删除;重算模式用有损 mp4,数值与原 CSV 会有差异(这正是它要显式呈现的)。
2026-09-13 22:04:01 +08:00

274 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Orisys Studio 配色规范
本文件记录客户端的品牌配色体系、它与 UI 设计稿的关系,以及每一处有意偏离设计稿
的理由。所有色值都存放在主题令牌文件里,控件代码中**不允许出现十六进制颜色**:
```
orisys-app/shared/src/orisys_app_shared/theme/colors_dark.json
orisys-app/shared/src/orisys_app_shared/theme/colors_light.json
```
规则由 `apps/studio/tools/ui_check.py` 的 `audit_palette()` 机械校验,pytest 套件
(`tests/test_ui_smoke.py::test_palette_rules_hold`)复用它,因此只有一个真值来源。
---
## 1. 设计稿色号 → 令牌映射
| 设计稿 | 角色 | 落地令牌 |
|---|---|---|
| `#003893` | 品牌主色 / 底图色 | `accent`、`plot_fn`(浅色);`arrow_canvas` 使用了更深的 logo 藏青 |
| `#4D62BE` | 交互蓝 | `accent_hover`(浅色)、`accent`(深色)、`focus_ring`(浅色) |
| `#12315D` | logo 实际藏青(见 §3.1) | `brand_bar_bg`、`arrow_canvas`(浅色) |
| `#FA02F3` | 洋红 | `flow_ramp` 第 3 档(原值保留) |
| `#FE5853` | 珊瑚 | `flow_ramp` 第 4 档(提亮为 `#FF8A6B`,见 §3.2);`danger` 家族取自同一色相 |
| `#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)
对比度用标准相对亮度公式实算(`tools/ui_check.py` 中的 `contrast_ratio`)。
正文门槛 4.5:1,图形/控件/状态指示门槛 3.0:1。
### 2.1 设计稿本身的实测结果
| 组合 | 比值 | 判定 |
|---|---|---|
| 白字 on `#003893` | 10.57 | AAA ✅ |
| 白字 on `#4D62BE` | 5.52 | AA ✅ |
| `#4D62BE` on `#D5D5D5` | 3.76 | 图形达标,12–13px 文字**不达标** ⚠️ |
| `#D5D5D5` vs 白卡 | 1.47 | 卡片层次偏弱 ⚠️ |
| `#D8FF00` on `#D5D5D5` | 1.27 | 不可用 ❌ |
| `#D8FF00` on `#003893` | 9.17 | 极佳 ✅ |
| `#FA02F3` on `#003893` | 3.20 | 达标 ✅ |
| `#FE5853` on `#003893` | 3.39 | 达标 ✅ |
| **`#FA02F3` vs `#FE5853`** | **1.06** | **色带中段的亮度平台 ❌** |
| 白字 on `#FE5853` | 3.12 | 珊瑚色不能承载白字 ❌ |
设计稿的色相选择是可靠的;三个霓虹色的**相对亮度**为 0.268 → 0.287 → 0.861,
色相沿 302° → 2° → 69° 单调前进 127°,作为"位移幅值 → 颜色"的顺序色带是正确用法。
问题在于中段:洋红与珊瑚亮度几乎相同(1.06:1),只靠 60° 色相差区分,在灰度
打印与色觉障碍下会糊成一片。
### 2.2 落地后
浅色主题(节选):白字 on `accent` 10.57,白字 on `accent_hover` 5.52,
`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` 对 `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 |
|---|---|---|---|
| 浅色 | 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",两主题均满足(深色 FN vs FY 靠 1.49 的亮度比
过关,浅色 FX vs FY 靠 97.1° 的色相差过关)。颜色在任何情况下都不是唯一编码:每个曲线有
文字标签,每张数值卡有同色通道徽标与通道名。
---
## 3. 对设计稿的四处有意偏离
### 3.1 品牌藏青用 logo 实际取色 `#12315D`
`logo.png` 与 `logo_topbar.png` 采样后字形的实际颜色都是 `#12315D`
(H215 S80.6% V36.5%),而设计稿写的是 `#003893`(H217 但 S100% V57.6%)。
两者同色相家族,但后者明显更亮更艳。顶栏品牌条与浅色主题的箭头画布采用 logo
实际色,以保证"标识色"与"界面色"一致。
### 3.2 色带第 4 档由 `#FE5853` 提亮为 `#FF8A6B`
见 §2.1:保留设计稿珊瑚色会在色带中段留下 1.06:1 的亮度平台。提亮后色带相对亮度
为 **0.031 → 0.128 → 0.268 → 0.405 → 0.861**,严格单调。
第 3 档洋红与第 5 档柠檬绿**保留设计稿原值不动**。
### 3.3 画布 `#D5D5D5` → `#E1E6EF`
`#D5D5D5` 是无彩色(H=0 S=0% L=84%)。现代浅色画布普遍在 96–98% 亮度,目的是让
白卡"浮起来";而在 `#D5D5D5` 上白卡只有 1.47:1,且 Qt 的 QSS 不支持 `box-shadow`,
卡片只能靠 1px 描边分离,观感会发平、发旧。改为品牌微蓝灰后:
- 保留设计稿"灰底 + 白卡"的角色分工;
- `#4D62BE` 在画布上的对比度从 3.76 升到 4.41。
### 3.4 补齐语义色与深色主题
设计稿没有 success/warning/error/info,也只有浅色版;本应用默认主题是深色。
同时原深色主题的语义色是一整套 Google Material 色
(`#8ab4f8` / `#34a853` / `#fbbc04` / `#ea4335` / `#1a73e8`),对一个测量仪器
产品属于即时可识别的品牌冲突,已全部替换为品牌衍生色。
`danger` 在深色主题被拆成两个令牌:`danger`(亮,用于卡片上的红字与状态指示)
与 `danger_fill`(暗,用于承载白字的危险按钮)—— 一个色值无法同时满足两个方向。
---
## 4. 品牌化箭头色带
设计稿的霓虹三色是**一张 colormap**(按位移幅值着色),不是三个分类色。因此它落在
箭头可视化上,而不是 UI 控件上:
```
flow_ramp = 画布色, #6E4AD0, #FA02F3, #FF8A6B, #D8FF00
```
- 第 1 档等于画布色,这样**零位移区域自然融入背景**;
- 由 `orisys_app_shared.acquisition.arrows.flow_colormap(theme)` 生成 256 档
BGR 查找表,经 `draw_arrows(colormap=...)` 注入 SDK(**SDK 无需改动**);
- LUT 缓存的键是 ramp 字符串本身,因此切换主题后立刻生效;
- 非法 ramp 永不抛异常,回退到内置默认色带。
箭头画布在两个主题里都是品牌深蓝(浅 `#12315D` / 深 `#0B1830`),这也是霓虹色带
唯一可用的背景 —— 色带第 3 档起对画布均 ≥3.0:1。**绘图区(`plot_bg`)保持浅色**,
所以浅色主题下霓虹只出现在箭头画布上,力曲线使用浅色安全的品牌三色。
---
## 5. 有记录在案的豁免
以下项目**不**被 `audit_palette()` 检查,理由是它们属于装饰而非状态指示:
| 项 | 现状 | 理由 |
|---|---|---|
| 装饰性 1px 容器描边 | `card_border` / `panel_border` / `plot_grid` / `separator`(1.16–1.45:1) | WCAG 1.4.11 针对交互控件的边界与状态指示;容器轮廓属于装饰 |
| 禁用态文字 | 深色 `text_muted` on `button_bg` = 4.07 | WCAG 1.4.3 明确豁免禁用控件 |
输入框的静止描边**不在豁免之列** —— 它被强制要求对 `input_bg`/`panel_bg`/`card_bg`/`toolbar_bg`
四种底均 ≥3.0:1(实测 3.08–4.09),这与 Material 3 描边式输入框的做法一致。焦点态同样由
`focus_ring` 保证 ≥3.0:1。
深色主题的 `accent` 对 `panel_bg` 为 3.09:1(设计稿原值 `#4D62BE`),贴线但达标;
若后续视觉上偏软,可单点提升为 `#5468C6`(白字仍为 5.03)。
---
## 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 | 单位、记录模式、预览 |
| 回放条(仅历史回放视图) | ~50 px | 录制选择、录制信息、进度条、位置、单位、重算深度、提示行 |
四点规则:
1. **传感器区是一个摘要按钮,不是两个下拉**。轨上只有 `BOX · box/sensor.json`;类型、配置
与「当前生效的标定 / 最近导出的系数」都在它打开的 `SensorDialog` 里(见
`docs/shell-lifecycle.md` §1.2)。摘要按钮属于状态轨,因此文本必须完整显示(见第 5 条)。
2. **来源选择随视图**:实时视图显示摄像机编号 + 搜索;用视频文件采集时显示「使用摄像机」;
回放视图不占位(回放条里有录制选择)。没有「来源」下拉,因此没有"先选来源才能用"的状态。
3. **导航按钮在品牌底上取品牌前景色**。`navButton` 用 `brand_bar_fg_muted`,当前视图用
`brand_bar_fg` + `navActive="true"`;两者都在 §2 的品牌底对比度审计里(7.67 / 12.94),
不新增色值。
4. **状态轨上的语义色一律算在 `toolbar_bg` 上审计**。记录状态(`status_idle` /
`status_running` / `status_connecting` / `status_error`)、丢帧提示(`warning`)与数据
有效性(`success` / `warning`)此前只检查 `panel_bg`,而它们实际画在顶栏底色上。
5. **同一时刻只有一个实心按钮**(`ToolbarWidget._refresh_emphasis`):
| 状态 | 实心 | 描边 |
|---|---|---|
| 空闲 | `运行`(`primaryButton`) | — |
| 连接中 | `取消连接`(`dangerButton`) | — |
| 采集中 | `开始记录`(`primaryButton`) | `停止`(`dangerOutline`) |
| 记录中 | `停止记录`(`dangerButton`) | `停止`(`dangerOutline`) |
`dangerOutline`:透明底 + `danger` 文字与描边,hover/pressed 才填充 `danger_fill`;
`:checked`(即"采集正在跑")仍保持描边——否则一圈红边会在会话开始后悄悄变成整块红色。
6. **状态轨上的事实不得被压缩**(`ui_check.rail_squeezed`)。中文、英文、1280 与 1440
四种组合下,轨上的按钮与下拉都必须显示完整文本;只有带 tooltip 的 `ShrinkableLabel`
允许在窗口极窄时被裁切。这也是"记录模式"下拉被挪进设置区的原因:英文的
`Start recording` + `Sensor config` 与它挤在同一行时,第一个被压缩的就是按钮文字。
7. **曲线表头只有符号,符号的颜色仍来自令牌**。全程总览 / 框选 / 返回最新三个按钮是
纯图标(`QPushButton[iconOnly="true"]`),图标由 `ui/icons.py` 用 `text_secondary`
实时绘制、切主题时按新令牌重画,因此配色审计的结论对它们同样成立,也不需要新增色值。
单位芯片 `QPushButton#unitBadge` 用 `card_bg` 底 + `card_border` 描边 + `text_secondary`
文字,hover 才换成 `text_primary` 文字与 `accent` 描边,它是"可点的状态"而不是实心主
按钮,所以不参与 §6.5 的"同一时刻只有一个实心按钮"计数(`QLabel#unitBadge` 仍是通道
徽标与传感器缩略图的样式,两者靠控件类型区分)。
8. **数值卡的小字用等宽体,且不会把面板顶宽**。选区统计(均值 · σ)画在数值卡内的 9pt
等宽小字上,与 16pt 的大号数字同一列;底部状态栏不再出现长句,因此曲线面板的宽度由
三个数值卡决定,而不是由一行统计文字决定。`ui_check.py` 的 `session browsing` 段断言
该小字 `width() >= sizeHint().width()`(中文与英文都不裁切)。
9. **两个视图共用一条状态轨**:来源槽是唯一的差别(实时=摄像机、回放=「选择录制视频…」),
因此轨上的文字宽度预算对两个视图是同一个。回放的视频名写在来源摘要里,**只在回放视图**
(`_refresh_status_rail` 按 `_view_preset` 决定),中英与 1280/1440 都由
`ui_check.rail_squeezed` 覆盖。
---
## 7. 三个顶层视图 = 同一窗口的三套布局
导航不再打开对话框,而是切换主窗口的布局(`StudioMainWindow._apply_view_preset`):
| 视图 | 面板 | 曲线模式 | 分割比例(3D, 2D, 曲线, 标定) |
|---|---|---|---|
| 实时监测 | 2D 图像 + 力曲线(+ 手指三维) | 跟随实时 | `0, 360, 1080, 0` |
| 历史回放 | 2D 图像 + 力曲线 | 读打开的录制(全程浏览) | `0, 360, 1080, 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"
$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 # 含 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`。