refactor: 拆出 Studio 状态显示并记录分步迁移计划

让状态显示独立于主窗口,保留录制快照注入入口。验证结果和完整门禁限制见 docs/architecture/studio-window-refactor-plan.zh.md。
This commit is contained in:
ykcheungab
2026-10-18 20:32:32 +08:00
parent cf3e638176
commit 0998009ec3
5 changed files with 580 additions and 203 deletions
+47 -33
View File
@@ -1,25 +1,50 @@
# Studio 软件架构(测量版)
# Studio 软件架构
本文不是"理想架构",而是**当前代码的真实依赖结构**:所有框图、数字与结论都由下面的脚本从源码里扫出来,
每条判断后面都跟着它的证据。配套的矢量图是 `studio-architecture.svg`(浏览器直接打开)。
本文的当前职责说明对应 2026-10-18 主窗口重构 S1;后续工作和验收由
[分步重构计划](studio-window-refactor-plan.zh.md) 维护。下方旧扫描图表保留为历史证据,
不能用其中的行数或“已完成”判断当前代码。
生成方式(可复现,改完代码重跑即可):
## 当前职责与数据流
> **脚本已不在工作区。** 下面两条命令指向 `D:\orisys_ws\.repro\` 下的一次性扫描脚本,该目录已清理
> (2026-09-27 工作区整理),扫描脚本本身没有保留。**本文的框图与数字仍然有效** —— 矢量图
> `studio-architecture.svg` 和 `studio-architecture.png` 已入库,是本文配套的、保留至今的产物。
> 如需重跑,按下面的 `PYTHONPATH` 与两条命令的口径重写等价脚本即可;结论口径见下方"统计口径"。
`app_window.py::StudioMainWindow` 创建控件和控制器并连接信号;本次从 3,181 行减至
3,052 行。它仍承担菜单、预览、标定交互、分析配置和运行协调,尚未完成薄窗口目标。
```powershell
$env:PYTHONPATH = 'D:\orisys_ws\orisys-app\shared\src;D:\orisys_ws\orisys-app\apps\studio\src'
& $py D:\orisys_ws\.repro\studio_arch_scan.py # 模块清单、包级依赖、环、Qt/GL/SDK 归属 ← 脚本已删除
& $py D:\orisys_ws\.repro\studio_arch_imports.py # 哪些模块在 import 时就拉进 Qt/GL/SDK,哪些是惰性 ← 脚本已删除
`status_presenter.py::StatusPresenter` 只持有工具栏,不持有窗口或会话控制器。
窗口在 GUI 线程收集一次性的 `RailState`,Presenter 负责 phase、来源、标定、有效性及
录制统计的文字和控件更新。快照不缓存、不反写状态;主题和语言刷新沿用原入口。
分析结果的传感器身份来自结果 provenance,进度读取现有 `AnalysisProgress.state()`,
不再通过 `ReplayController._progress` 访问其私有控件。
```mermaid
graph TD
W[StudioMainWindow 装配与接线] --> S[SessionController 会话与遥测]
W --> R[ReplayController 回放与分析执行]
W --> L[ViewLayoutController 布局]
W --> D[DisplayPrefsController 显示偏好]
W --> C[RecordingCoordinator 记录与设备确认]
W --> P[StatusPresenter 状态显示]
F[RailState 只读显示事实] --> P
P --> T[ToolbarWidget 状态轨]
W --> A[AcquirePresenter 实时呈现]
W --> V[FrameView 二维与共享深度范围]
```
统计口径:行数含空行;`fan-in` = 有多少模块 import 它;`fan-out` = 它 import 了多少模块(同一模块多次
import 只算一条边)。
会话状态仍属于 `SessionController`,录制状态仍属于 `RecordingCoordinator`。
`_record_stats` 保留为 UI 检查和截图的注入入口,实际状态选择及显示已委托 Presenter。
关闭中的录制继续读取实时统计,完成后读取最终报告;丢失计数包括未能记下的 gap。
设备确认与录制仍走采集线程通道;本次未改变定时器、停止等待或 3D 懒加载。
---
验证入口:`test_status_presenter.py` 可在没有主窗口和设备的情况下检查状态显示;
`test_ui_smoke.py`、`test_replay_wiring.py` 和 `tools/ui_check.py` 覆盖窗口集成。
执行结果、环境及尚未验证范围记录在实施计划。状态转发暂留用于现有调用方,后续按职责迁移,
不增加新的全局状态容器或将剩余逻辑全部并入回放控制器。
## 历史测量的适用范围
以下第 1–6 节及配套 `studio-architecture.svg`、`studio-architecture.png` 是旧扫描快照。
原扫描的对应 revision 与完整运行输入不可用,生成脚本已于 2026-09-27 清理,无法原样复跑。
数字口径为含空行行数;fan-in/out 为模块导入关系。旧测试计数、无环结论和完成状态均未在本次
重新全量测量,不是当前保证;后续重构顺序以实施计划为准。
## 1. 分层视图(真实依赖方向)
@@ -289,35 +314,24 @@ graph TB
7. **验证面 > 生产面**:19.7k 行测试/工具 vs 14.7k 行 studio 生产代码。好处是回归门硬;代价是每次改接口
的成本被放大,尤其是 `ui_check` 里的字符串/布局断言(拆 `app_window` 时为保住这套门而留的转发,就是这个代价)。
### 一句话结论
### 历史评估结论
**组织是合理的、容易维护的**(无环、分层单向、共享层无 Qt、危险知识集中在有测试的地方)。
原来唯一的明确单点 `app_window.py` 已按职责拆成
`session_controller` / `replay_controller` / `view_layout` / `display_prefs` + 标定服务,
窗口退回装配与转发;每个新模块都有自己的公开接口单测(会话元数据 15 + 四个控制器 37 +
标定服务 4 = 56 项),且在不需要窗口的情况下可测。
下一步的性价比最高项因此换成了下面第 2 条(把"现在对"变成"以后不会错")与第 5 条
(最脆的资产 `ui_check.py` 现在是 3401 行单文件)。
这是旧扫描时的评估结论;当前仍有窗口职责待迁移,实施顺序见下节链接。
## 7. 建议(按性价比排序)
## 后续迁移
| # | 建议 | 收益 | 成本/风险 | 回归门 |
|---|---|---|---|---|
| 1 | ~~**拆 `app_window`**~~ **已完成**(会话 → `session_controller`、回放 → `replay_controller`、布局 → `view_layout`、显示偏好 → `display_prefs`、标定规则 → `calibration_service`;窗口 3044 → 2309 行) | 改动热点从 1 个 3000 行文件变成 5 个 200–650 行文件 + 一个装配器;每个控制器可脱离窗口单测(新增 41 项) | ~~中~~(已付:纯搬运 + 一层转发,未改行为) | `test_ui_smoke`/`test_scene3d_view`/`ui_check` 全绿;`git diff` 只应有移动与转发 |
| 1b | **删掉过渡转发**:窗口里那约 40 个 `self._x` 转发是给 tests/`ui_check` 用的桥,下一次改动落在哪个控制器,就顺手把该控制器的转发与测试调用点改成 `self.<controller>.*` | 窗口真正退到"只有装配";转发留着会慢慢变成新的隐式耦合 | 小-中(每次改动顺带;一次性清理会大面积改测试) | 全套 + `ui_check` |
| 2 | **给共享层加"依赖规则测试"**:AST 扫描断言 `shared` 不得 eager import PySide6/OpenGL(现状成立),以及 `studio.scene3d` 不得 import `acquisition`;把 `force → orisys` 的现状登记为"已知例外"或改为惰性 —— *部分已完成*:`apps/studio/tests/test_studio_binding_purity.py` 与 `apps/autocal/tests/test_binding_purity.py` 已经强制"只允许 PySide6"(含无作用域枚举的检查,且用真 binding 内省而不是手写枚举名单);`studio.scene3d` 与 `force → orisys` 两条仍未加 | 把"现在对"变成"以后不会错";顺手暴露第 4 条风险 | 小(一个测试文件,用现成扫描脚本改) | 新测试 + 现有套件 |
| 3 | **消歧命名**:`scene3d/panel.py` → `scene3d/controls.py`;`ui/mesh3d_panel.py` → `ui/scene3d_host.py`;`scene3d/replay` 与 `record/playback` 在 docstring 里互相指路 | 新人少走 3 次错门 | 小(改名 + import 更新),但会触及 ui_check 的引用 | 全套 + `ui_check` |
| 4 | **把 `force → SDK` 改为惰性**(`calibration_io` 里改成函数内 import,或把 SDK 相关函数拆出去) | 力曲线的纯计算(history/filters/scale)可在无 SDK 环境测试 | 中(要区分哪些函数真的需要 SDK) | 现有力测试 + 无 SDK 的 import 检查 |
| 5 | **`ui_check` 拆模块**(按场景拆 `tools/ui_checks/*.py`,保留单一入口与退出码) | 最脆的资产变成可分工、可 diff 的几块 | 中(3401 行搬运,语义不变) | 检查项计数不变(587 项 0 失败) |
| 6 | **测试私有访问清单化**:把所有 `obj._x` 访问集中到 `tests/support/` 一层小适配器(拆 `app_window` 时保留的转发就是为了不一次性打碎它们) | 内部重构不再一次性打碎几十个测试 | 小-中 | 全套 |
| 7 | **补上真机与 ARM 的验收**(本机无设备、无板卡):清零/标定确认、`VRAW` 链失败后的状态、2 小时 GUI 长时、Jetson Orin Nano / QCS6490 报告 | 把"代码上成立"变成"设备上成立"——这是当前最大的空白 | 中(要设备与板卡) | 阶段 9 的验收清单 |
不建议做的事:**不要**为了"看起来分层漂亮"把 `scene3d` 从 studio 搬进 shared(第 3 条第 5 项理由不硬,
但现在搬动没有收益,反而打断三维子系统已经稳定的接口);也**不要**为了统一而重写 `ui_check`。
当前工作由 [分步重构计划](studio-window-refactor-plan.zh.md) 的 S2–S6 管理,
包括界面构建、标定交互、分析配置、运行与预览以及显示入口和兼容桥收尾。
每一步完成后更新本节之前的当前职责;历史指标不随搬运代码自动改写。
---
_本文与图由源码扫描生成;扫描脚本 `.repro/studio_arch_scan.py`、`.repro/studio_arch_imports.py`
_以上历史快照与图由源码扫描生成;扫描脚本 `.repro/studio_arch_scan.py`、`.repro/studio_arch_imports.py`
与图表脚本 `.repro/make_studio_svg.py` → `.repro/svg_to_png.py`、原始数据 `.repro/studio_arch.json`、
`.repro/studio_arch_imports.json` **均已在 2026-09-27 的工作区整理中删除**,未迁移。保留下来的是
`.repro/` 之外的本文与图本身(`studio-architecture.svg`、`studio-architecture.png`)。