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

16 KiB
Raw Blame History

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. 复核方式

$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。