文档:刷新架构报告的指标与结论(app_window 拆分已落地)

- What: `docs/architecture/studio-architecture.md` 的数字与结论按当前源码重扫后刷新:
  - 分层图/纯文本图:`app_window` 3044 → **2309 行 / fan-out 37**,并新增一层
    "②b Studio 控制器"(session_controller 437 / replay_controller 641 / view_layout 398 /
    display_prefs 227);领域服务与共享层的行数一并更新(replay 291、calibration_service 194、
    coordinator 619、camera 238、view 757、asset_source 256、gl_backend 870;
    shared scene3d 4565 / acquisition 2627 / record 3008 / force 3057 / 基础设施 1507)。
  - 边界规则:模块数 89(studio 32 + shared 57)→ **94(studio 37 + shared 57)**,环 0 个不变。
  - 指标表:新增 `replay_controller`(641) 与 `session_controller`(437) 两行,并写明
    "app_window 的 fan-out 仍是最高(37),因为它本来就是装配器——但规则已经不在它里面"。
  - 生产面/验证面:studio 生产 14 655 行、shared 14 764、测试 15 639(shared 7226/334 项 +
    studio 8413/310 项)、工具 4046(ui_check 3401);验证面 19.7k > 生产面 14.7k。
  - 评估第 1 条与"一句话结论"改写为:上帝对象已按职责拆开,并**诚实记账**留下的东西——
    约 40 个过渡转发(给 tests/`ui_check` 的桥,属过渡层)、仍留在窗口的预览与 2D 渲染
    (它们同时属于面板与相机),以及 fan-out 从 36 升到 37 的原因。
  - 建议表第 1 条标记为已完成(写清五个去处与新增 41 项控制器单测),新增第 1b 条
    "下次改动顺手删掉对应转发",并补第 7 条"真机与 ARM 验收"(本机无设备/板卡,是当前最大空白)。
- What: `studio-architecture.svg` / `.png` 由 `.repro/make_studio_svg.py` 重新生成:
  窗口框改为"2309 行 · 158 方法 · fan-out 37 · 装配 + 信号转发",右侧四个控制器小框,
  面板/服务/共享层/验证面的行数与测试项数同步;底部结论改为"已按职责拆开 + 剩下的胶水"。
  顺带修好 `svg_to_png.py` 里写死的 1560×1180(现在读 SVG 自己的尺寸),否则海报加高后会被裁掉。
- Why: 这份报告是"判断组织是否合理"的依据,数字或结论一旦过期就会误导决策;
  拆分改变最大的一处正是它第 1 条风险与第 1 条建议,必须同步。
- Verification: 图表脚本重跑无异常(SVG well-formed,PNG 1560×1210 已目视检查);
  同批源码的验证为 pytest 644 passed / 0 failed / 15 skipped(offscreen)、
  tools/ui_check.py 587 项 0 失败、.repro/locale_check.py 0 缺失。
- Not in this change: 不改代码;不改 `.repro` 下的扫描脚本口径(仍以扫描输出为准)。
This commit is contained in:
ykcheungab
2026-09-15 10:02:37 +08:00
parent 81e39ca868
commit b37c861254
3 changed files with 128 additions and 80 deletions
+83 -44
View File
@@ -21,12 +21,18 @@ import 只算一条边)。
```mermaid
graph TB
subgraph L0["① 入口"]
MAIN["main.py<br/>110 行 · QApplication + 主题 + 首选项"]
MAIN["main.py<br/>109 行 · QApplication + 主题 + 首选项"]
end
subgraph L1["② 窗口外壳(只有它能看见全部 UI)"]
WIN["app_window.py · StudioMainWindow<br/>3044 行 · 158 个方法 · fan-out 36"]
WIN["app_window.py · StudioMainWindow<br/>2309 行 · 158 个方法 · fan-out 37"]
BAR["brand_bar · toolbar · record_status"]
end
subgraph L15["②b Studio 控制器(一个变更驱动一个文件)"]
SESS["session_controller.py<br/>会话与相位 · 437 行"]
RPLC["replay_controller.py<br/>回放策略 · 641 行"]
LAY["view_layout.py<br/>面板布局 · 398 行"]
DISP["display_prefs.py<br/>显示偏好 · 227 行"]
end
subgraph L2["③ 面板(各自的画布与控件)"]
ARROW["ui/arrow_panel<br/>2D 图像 · 519 行"]
MESH["ui/mesh3d_panel<br/>3D 宿主容器 · 405 行"]
@@ -35,24 +41,28 @@ graph TB
DLG["sensor_dialog · plot_settings_dialog · cal_point_dialog"]
end
subgraph L3["④ Studio 领域服务(无 Qt 的与有 Qt 的分开)"]
REC["recording.py<br/>记录协调 · 268 行"]
REP["replay.py<br/>回放引擎 · 286 行(QObject)"]
CALSVC["calibration_service.py<br/>131 行"]
COORD["scene3d/coordinator.py<br/>560 行 · **无 Qt**"]
CAM["scene3d/camera.py<br/>210 行 · **无 Qt**"]
VIEW["scene3d/view.py<br/>758 行 · 3D 唯一入口"]
ASSET["scene3d/asset_source.py<br/>257 行 · 场景加载线程"]
GLB["scene3d/gl_backend.py<br/>871 行 · **GL 只在运行时惰性 import**"]
REC["recording.py<br/>记录协调 · 267 行"]
REP["replay.py<br/>回放引擎 · 291 行(QObject)"]
CALSVC["calibration_service.py<br/>标定文件与归属 · 194 行 · 无 Qt"]
COORD["scene3d/coordinator.py<br/>619 行 · **无 Qt**"]
CAM["scene3d/camera.py<br/>238 行 · **无 Qt**"]
VIEW["scene3d/view.py<br/>757 行 · 3D 唯一入口"]
ASSET["scene3d/asset_source.py<br/>256 行 · 场景加载线程"]
GLB["scene3d/gl_backend.py<br/>870 行 · **GL 只在运行时惰性 import**"]
end
subgraph L4["⑤ 共享领域层(采集/记录/力/三维契约,无 Qt 依赖)"]
AQ["acquisition<br/>2343 行 · worker / packet / sinks / 图像后端"]
RC["record<br/>2673 行 · 录像读写 / CSV / session.json"]
FORCE["force<br/>2645 行 · 力曲线源 / 标定 / 滤波"]
S3D["scene3d<br/>4057 行 · 契约 / 采样 / 合成 / 包格式 / 签名"]
AQ["acquisition<br/>2627 行 · worker / packet / sinks / 图像后端"]
RC["record<br/>3008 行 · 录像读写 / CSV / session.json"]
FORCE["force<br/>3057 行 · 力曲线源 / 标定 / 滤波"]
S3D["scene3d<br/>4565 行 · 契约 / 采样 / 合成 / 包格式 / 签名"]
MISC["camera · prefs · paths · i18n · theme · logging"]
end
MAIN --> WIN
WIN --> BAR
WIN --> SESS
WIN --> RPLC
WIN --> LAY
WIN --> DISP
WIN --> ARROW
WIN --> MESH
WIN --> PLOTS
@@ -86,6 +96,7 @@ graph TB
```
main.py
└─ app_window.py ──┬─ 外壳: brand_bar / toolbar / record_status
├─ 控制器: session_controller · replay_controller · view_layout · display_prefs
├─ 面板: arrow_panel(2D) · mesh3d_panel(3D) · force_plots · cal_panel · 对话框
├─ 领域服务: recording · replay · calibration_service · scene3d.{view,asset_source,coordinator,keys}
└─ 共享层(无 Qt): acquisition · record · force · scene3d · camera/prefs/paths/i18n/theme/logging
@@ -174,7 +185,7 @@ graph TB
| 规则 | 是否成立 | 证据 |
|---|---|---|
| 模块级无循环依赖 | ✅ | 扫描 **89 个模块**(studio 32 + shared 57)的 import:**环 0 个** |
| 模块级无循环依赖 | ✅ | 扫描 **94 个模块**(studio 37 + shared 57)的 import:**环 0 个** |
| 共享层不依赖 Qt | ✅(有一条惰性例外) | `import` 时拉进 PyQt5 的 21 个模块**全部**在 `orisys_studio.*`;`shared/theme/qss.py` 用 PyQt5 **画图标时**才 import(函数内 + try 保护) |
| OpenGL 从不在 import 时拉入 | ✅ | 唯一使用者 `scene3d/gl_backend.py` 是函数内惰性 import;`widget.py` 同理 |
| 三维调度与相机无 Qt,可脱离 GUI 测试 | ✅ | 新解释器里 `import orisys_studio.scene3d.coordinator` / `.camera` → 不含 PyQt5/OpenGL/SDK |
@@ -189,29 +200,38 @@ graph TB
| 模块 | 行数 | fan-out | 说明 |
|---|---:|---:|---|
| `orisys_studio.app_window` | 3044 | **36** | 158 个方法,其中 50 个 `_on_*` 处理器;唯一 fan-in=1 的中心 |
| `orisys_studio.ui.force_plots` | 1243 | 8 | 曲线、量程、光标、概览、单位徽章都在一个类里 |
| `orisys_studio.scene3d.gl_backend` | 871 | 2 | 着色器、打包、纹理、统计;被 4 个模块依赖 |
| `orisys_studio.ui.cal_panel` | 793 | 7 | 标定表 + 采样 + 拟合入口 |
| `orisys_studio.scene3d.view` | 758 | 8 | 3D 的唯一入口(刻意的) |
| `orisys_studio.app_window` | 2309 | **37** | 158 个方法,其中约 40 个是给测试/`ui_check` 的一层转发;装配 + 信号转发 |
| `orisys_studio.ui.force_plots` | 1242 | 8 | 曲线、量程、光标、概览、单位徽章都在一个类里 |
| `orisys_studio.scene3d.gl_backend` | 870 | 2 | 着色器、打包、纹理、统计;被 4 个模块依赖 |
| `orisys_studio.ui.cal_panel` | 792 | 7 | 标定表 + 采样 + 拟合入口 |
| `orisys_studio.scene3d.view` | 757 | 8 | 3D 的唯一入口(刻意的) |
| `orisys_studio.replay_controller` | 641 | 5 | 回放策略(本轮从 app_window 拆出) |
| `orisys_studio.scene3d.coordinator` | 619 | 3 | 三维调度(**无 Qt**) |
| `orisys_studio.session_controller` | 437 | 3 | 会话/相位/遥测(本轮从 app_window 拆出) |
`app_window` 的 **fan-out 仍然最高(37)**,因为它本来就是装配器:它 import 的模块多,
但**规则**已经不在它里面——单次功能改动落在 `session_controller`(会话)、
`replay_controller`(回放)、`view_layout`(布局)、`display_prefs`(显示偏好)或
`calibration_service`(标定)之一,而不是"改哪都要动同一个 2300 行文件"。
**稳定核心(被依赖最多)**:`scene3d.contracts`(22) > `i18n`(16) > `paths`(11) = `theme`(11) >
`record.sample`(8) > `acquisition.models`(5) = `force.filters`(5) = `force.session`(5) = `scene3d.digest`(5)…
**验证面**:共享库测试 22 文件 / 5992 行;应用测试 10 文件 / 5641 行;工具 `ui_check.py` 3168 行、
`bench_scene.py` 608、`soak_scene.py` 503、`validate_scene.py` 302、`ui_snapshot.py` 292、`probe_gl.py` 192。
**验证面**:共享库测试 22 文件 / 7226 行 / 334 项;应用测试 15 文件 / 8413 行 / 310 项;工具
`ui_check.py` 3401 行(587 项 UI 断言)、`bench_scene.py`、`soak_scene.py`、`validate_scene.py`、
`ui_snapshot.py`、`probe_gl.py`。
**生产面 vs 验证面**(同一口径:含空行):
| | 行数 | 其中三维 |
|---|---:|---:|
| `apps/studio/src`(生产) | 13 376 | 4 326 |
| `shared`(生产,无 Qt) | 14 820 | 4 576 |
| 测试(shared 5992 + studio 5641) | 11 633 | — |
| 工具(ui_check 3168 + scene_assets 工具 2104) | 5 272 | — |
| `apps/studio/src`(生产,37 模块) | 14 655 | — |
| `shared`(生产,57 模块,无 Qt) | 14 764 | 4 565 |
| 测试(shared 7226 + studio 8413) | 15 639 | — |
| 工具(ui_check 3401 + scene_assets 工具 645) | 4 046 | — |
即**验证面(16.9k 行)比 studio 的生产代码(13.4k 行)还大**。共享层里记录 2673、力 2645、采集 2343 行,
三维 4576 行——三维不是附属功能,它和"记录/力"是同一量级的子系统。
即**验证面(19.7k 行)比 studio 的生产代码(14.7k 行)还大**。共享层里记录 3008、力 3057、采集 2627 行,
三维 4565 行——三维不是附属功能,它和"记录/力"是同一量级的子系统。
## 6. 评估:合理与不合理的地方
@@ -232,11 +252,21 @@ graph TB
### 不合理、会随时间变贵的地方(按严重程度)
1. **`app_window.py` 是上帝对象**:3044 行 / 158 方法 / fan-out 36。它同时扮演四个角色——
①视图装配器 ②会话与 worker 生命周期控制器 ③业务决策(单位回退、量程、重算/回放互斥、面板尺寸)
④持久化与诊断聚合。后果:任何功能改动都要动它(本次两轮修 bug 里,`_load_replay_video`、
`_sync_replay_actions`、`_update_run_enabled`、`_render_*` 全在同一个文件里互相影响);
它是**唯一** fan-in=1 的中心,也就是说没有第二个人能独立改它的任一部分而不碰冲突热点。
1. **`app_window.py` 曾经是上帝对象(已拆)**:拆之前 3044 行 / 158 方法 / fan-out 36,
四个角色(视图装配、会话与 worker 生命周期、业务决策、持久化与诊断)都在一个文件里。
现在它 2309 行 / fan-out 37,规则分到四个控制器 + 标定服务:
`session_controller`(谁在跑、哪一相位、报了什么数)、`replay_controller`(回放策略与重算)、
`view_layout`(哪些面板、什么布局、哪个视图)、`display_prefs`(单位/量程/箭头/fps)、
`calibration_service`(标定文件与"这份标定是谁的")。
**仍然留在窗口里的**:装配与信号转发、菜单/对话框、状态栏与轨道、
预览(`_start_preview`/`_stop_preview`/`_on_preview_tick`)与 2D 渲染
(`_show_image_frame`/`_render_run_image`/`_depth_range`)——它们同时属于面板与相机,
目前没有第二个变更驱动,硬拆只会把耦合搬家。
**代价与风险(诚实记账)**:为了让 tests 与 `tools/ui_check`(它们通过窗口驱动 UI)
保持全绿,窗口保留了一层转发(约 40 个成员)。它们是**过渡层**,不是最终形态:
下一次改动落在某个控制器时,应顺手把对应的转发与测试调用点改成 `self.<controller>.*`。
另外窗口的 fan-out 反而从 36 升到 37(它多 import 了四个控制器)——这正是"装配器"
应有的样子:它知道所有人,但不再替所有人做决定。
2. **三个"panel"名字打架**:`ui/arrow_panel`(2D 面板)、`ui/mesh3d_panel`(3D 宿主容器)、
`scene3d/panel`(3D 参数控件)。新人第一次找"面板加个开关"要在三个文件里试。同类问题还有
`scene3d/replay.py`(studio 回放引擎) 与 `shared/record/playback.py`(录像读取)。
@@ -246,35 +276,44 @@ graph TB
4. **共享层 `force` 硬依赖 SDK**:`calibration_io` 在 import 时 `import orisys`,于是"共享层可以脱离
SDK 做单元测试"这条**只对三维成立,对力不成立**(力相关的测试必须装 SDK)。这也解释了为什么
离线工具与力曲线测试总是整套跑。
5. **`ui_check.py` 3168 行单文件**:它是产品 UI 的"第二份规格"(587 项断言),但没有分层,改 UI 就改它,
5. **`ui_check.py` 3401 行单文件**:它是产品 UI 的"第二份规格"(587 项断言),但没有分层,改 UI 就改它,
且它引用了不少产品私有属性/组件名。它同时是**最有价值**的回归门和**最脆**的资产。
6. **测试对私有成员的依赖**:`tests` 里直接读 `view._camera`、`widget._renderer`、`coordinator._inflight`、
`loader._pending`、`panel._descriptor` 等。接口一改就要改测试(这几轮已经通过
`attach_view` / `describe` / `render_totals` / `_snapshot_diagnostic` 之类的公开缝减少了一批)。
7. **验证面 > 生产面**:16k 行测试/工具 vs 12.9k 行 studio 生产代码。好处是回归门硬;代价是每次改接口
的成本被放大,尤其是 `ui_check` 里的字符串/布局断言。
7. **验证面 > 生产面**:19.7k 行测试/工具 vs 14.7k 行 studio 生产代码。好处是回归门硬;代价是每次改接口
的成本被放大,尤其是 `ui_check` 里的字符串/布局断言(拆 `app_window` 时为保住这套门而留的转发,就是这个代价)。
### 一句话结论
**组织是合理的、容易维护的**(无环、分层单向、共享层无 Qt、危险知识集中在有测试的地方),
**但有一个明确的单点:`app_window.py`**。它现在是"能维护"和"改一处要被它绊住"之间的分界线;
再往上加功能(下一个面板、下一种传感器、下一个回放能力)之前,把它拆开是性价比最高的一件事。
**组织是合理的、容易维护的**(无环、分层单向、共享层无 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`** 为三个控制器:`SessionController`(worker 生命周期 + sinks + 记录)、`RunViewPresenter`(面板可见性/尺寸/单位/量程)、`ReplayCoordinator`(把窗口里回放相关逻辑并进已有的 `replay.py`),窗口只剩装配 + 信号转发 | 改动热点从 1 个 3044 行文件变成 4 个 400–700 行文件;并行改动不再互相踩 | 中(纯搬运,不改行为);风险是搬运时把某个"隐式顺序依赖"弄丢 | 现有 `test_ui_smoke`/`test_scene3d_view`/`ui_check` 全绿;`git diff` 只应有移动与转发 |
| 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 PyQt5/OpenGL(现状成立),以及 `studio.scene3d` 不得 import `acquisition`;把 `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 的几块 | 中(3168 行搬运,语义不变) | 检查项计数不变(587 项 0 失败) |
| 6 | **测试私有访问清单化**:把所有 `obj._x` 访问集中到 `tests/support/` 一层小适配器 | 内部重构不再一次性打碎几十个测试 | 小-中 | 全套 |
| 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`。
---
_本文与图由源码扫描生成;扫描脚本在 `.repro/studio_arch_scan.py`、`.repro/studio_arch_imports.py`,
原始数据在 `.repro/studio_arch.json`、`.repro/studio_arch_imports.json`。_
_本文与图由源码扫描生成;扫描脚本在 `.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`。数字口径:行数含空行,`fan-out` = 该模块 import 了多少个模块。_
_生成时间:拆分 `app_window`(A1/A2 + B + C1 + C2 + 标定收尾 + 清零确认修复)之后。_
Binary file not shown.

Before

Width:  |  Height:  |  Size: 267 KiB

After

Width:  |  Height:  |  Size: 294 KiB

+45 -36
View File
@@ -1,5 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1560" height="1180" viewBox="0 0 1560 1180" font-family="Segoe UI, sans-serif">
<rect width="1560" height="1180" fill="#0f1420"/>
<svg xmlns="http://www.w3.org/2000/svg" width="1560" height="1210" viewBox="0 0 1560 1210" font-family="Segoe UI, sans-serif">
<rect width="1560" height="1210" fill="#0f1420"/>
<text x="40" y="42" fill="#e8edf7" font-size="23" font-weight="700">Orisys Studio 架构(按源码实际 import 关系生成)</text>
<text x="40" y="66" fill="#9fb0c8" font-size="12.5">绿 = import 时不拉 Qt/OpenGL 蓝 = 需要 Qt 橙 = GL 仅运行时惰性加载 紫 = 验证面 数字为行数(含空行)</text>
<rect x="30" y="84" width="1500" height="62" rx="10" fill="#161d2c" stroke="#27324a" stroke-width="1"/>
@@ -22,92 +22,100 @@
<rect x="220" y="100" width="520" height="34" rx="7" fill="#1d2b45" stroke="#4a5a76" stroke-width="1.3"/>
<text x="231" y="121" fill="#e8edf7" font-size="13" font-family="Consolas, monospace">main.py</text>
<text x="231" y="139" fill="#9fb0c8" font-size="11">110 行 · QApplication · 主题 · 首选项 · 单实例</text>
<rect x="220" y="200" width="700" height="76" rx="7" fill="#1d2b45" stroke="#f87171" stroke-width="1.3"/>
<rect x="220" y="200" width="700" height="76" rx="7" fill="#1d2b45" stroke="#7dd3fc" stroke-width="1.3"/>
<text x="231" y="221" fill="#e8edf7" font-size="13" font-family="Consolas, monospace">app_window.py · StudioMainWindow</text>
<text x="231" y="239" fill="#9fb0c8" font-size="11">3044 行 · 158 个方法 · 50 个 _on_* 处理器 · fan-out 36</text>
<text x="231" y="255" fill="#7dd3fc" font-size="11" font-family="Consolas, monospace">唯一 fan-in = 1 的中心:改任何一处都会碰它</text>
<rect x="950" y="216" width="170" height="44" rx="7" fill="#1d2b45" stroke="#4a5a76" stroke-width="1.3"/>
<text x="961" y="237" fill="#e8edf7" font-size="13" font-family="Consolas, monospace">brand_bar</text>
<text x="961" y="255" fill="#9fb0c8" font-size="11">146 行</text>
<rect x="1140" y="216" width="170" height="44" rx="7" fill="#1d2b45" stroke="#4a5a76" stroke-width="1.3"/>
<text x="1151" y="237" fill="#e8edf7" font-size="13" font-family="Consolas, monospace">toolbar</text>
<text x="1151" y="255" fill="#9fb0c8" font-size="11">760 行</text>
<rect x="1330" y="216" width="170" height="44" rx="7" fill="#1d2b45" stroke="#4a5a76" stroke-width="1.3"/>
<text x="1341" y="237" fill="#e8edf7" font-size="13" font-family="Consolas, monospace">record_status</text>
<text x="1341" y="255" fill="#9fb0c8" font-size="11">205 行</text>
<text x="231" y="239" fill="#9fb0c8" font-size="11">2309 行 · 158 个方法 · fan-out 37(含四个控制器)</text>
<text x="231" y="255" fill="#7dd3fc" font-size="11" font-family="Consolas, monospace">装配 + 信号转发;状态与规则在四个控制器里(右侧)</text>
<rect x="950" y="200" width="270" height="44" rx="7" fill="#1d2b45" stroke="#4a5a76" stroke-width="1.3"/>
<text x="961" y="221" fill="#e8edf7" font-size="12" font-family="Consolas, monospace">session_controller.py</text>
<text x="961" y="239" fill="#9fb0c8" font-size="11">会话 · 相位 · 437 行</text>
<text x="961" y="255" fill="#7dd3fc" font-size="11" font-family="Consolas, monospace">worker / 遥测 / 结束原因</text>
<rect x="1235" y="200" width="270" height="44" rx="7" fill="#1d2b45" stroke="#4a5a76" stroke-width="1.3"/>
<text x="1246" y="221" fill="#e8edf7" font-size="12" font-family="Consolas, monospace">replay_controller.py</text>
<text x="1246" y="239" fill="#9fb0c8" font-size="11">回放策略 · 641 行</text>
<text x="1246" y="255" fill="#7dd3fc" font-size="11" font-family="Consolas, monospace">播放 · 打开 · 重算</text>
<rect x="950" y="250" width="270" height="44" rx="7" fill="#1d2b45" stroke="#4a5a76" stroke-width="1.3"/>
<text x="961" y="271" fill="#e8edf7" font-size="12" font-family="Consolas, monospace">view_layout.py</text>
<text x="961" y="289" fill="#9fb0c8" font-size="11">面板布局 · 398 行</text>
<text x="961" y="305" fill="#7dd3fc" font-size="11" font-family="Consolas, monospace">可见性 · 预设 · 3D 开关</text>
<rect x="1235" y="250" width="270" height="44" rx="7" fill="#1d2b45" stroke="#4a5a76" stroke-width="1.3"/>
<text x="1246" y="271" fill="#e8edf7" font-size="12" font-family="Consolas, monospace">display_prefs.py</text>
<text x="1246" y="289" fill="#9fb0c8" font-size="11">显示偏好 · 227 行</text>
<text x="1246" y="305" fill="#7dd3fc" font-size="11" font-family="Consolas, monospace">单位 · 量程 · 箭头 · fps</text>
<rect x="220" y="352" width="238" height="92" rx="7" fill="#1d2b45" stroke="#4a5a76" stroke-width="1.3"/>
<text x="231" y="373" fill="#e8edf7" font-size="13" font-family="Consolas, monospace">ui/arrow_panel</text>
<text x="231" y="391" fill="#9fb0c8" font-size="11">2D 图像 · 519 行</text>
<text x="231" y="391" fill="#9fb0c8" font-size="11">2D 图像 · 518 行</text>
<text x="231" y="407" fill="#7dd3fc" font-size="11" font-family="Consolas, monospace">→ image_backend(共享)</text>
<rect x="478" y="352" width="238" height="92" rx="7" fill="#1d2b45" stroke="#4a5a76" stroke-width="1.3"/>
<text x="489" y="373" fill="#e8edf7" font-size="13" font-family="Consolas, monospace">ui/mesh3d_panel</text>
<text x="489" y="391" fill="#9fb0c8" font-size="11">3D 宿主容器 · 405 行</text>
<text x="489" y="391" fill="#9fb0c8" font-size="11">3D 宿主容器 · 404 行</text>
<text x="489" y="407" fill="#7dd3fc" font-size="11" font-family="Consolas, monospace">→ scene3d/view</text>
<rect x="736" y="352" width="238" height="92" rx="7" fill="#1d2b45" stroke="#4a5a76" stroke-width="1.3"/>
<text x="747" y="373" fill="#e8edf7" font-size="13" font-family="Consolas, monospace">ui/force_plots</text>
<text x="747" y="391" fill="#9fb0c8" font-size="11">三轴曲线 · 1243 行</text>
<text x="747" y="391" fill="#9fb0c8" font-size="11">三轴曲线 · 1242 行</text>
<text x="747" y="407" fill="#7dd3fc" font-size="11" font-family="Consolas, monospace">→ force.history</text>
<rect x="994" y="352" width="238" height="92" rx="7" fill="#1d2b45" stroke="#4a5a76" stroke-width="1.3"/>
<text x="1005" y="373" fill="#e8edf7" font-size="13" font-family="Consolas, monospace">ui/cal_panel</text>
<text x="1005" y="391" fill="#9fb0c8" font-size="11">标定表 · 793 行</text>
<text x="1005" y="391" fill="#9fb0c8" font-size="11">标定表 · 792 行</text>
<text x="1005" y="407" fill="#7dd3fc" font-size="11" font-family="Consolas, monospace">→ force 标定</text>
<rect x="1252" y="352" width="238" height="92" rx="7" fill="#1d2b45" stroke="#4a5a76" stroke-width="1.3"/>
<text x="1263" y="373" fill="#e8edf7" font-size="13" font-family="Consolas, monospace">对话框 ×3</text>
<text x="1263" y="391" fill="#9fb0c8" font-size="11">sensor / plot / cal_point</text>
<rect x="220" y="524" width="296" height="56" rx="7" fill="#1d2b45" stroke="#4a5a76" stroke-width="1.3"/>
<text x="231" y="545" fill="#e8edf7" font-size="13" font-family="Consolas, monospace">recording.py</text>
<text x="231" y="563" fill="#9fb0c8" font-size="11">记录协调 · 268 行</text>
<text x="231" y="563" fill="#9fb0c8" font-size="11">记录协调 · 267 行</text>
<rect x="538" y="524" width="296" height="56" rx="7" fill="#1d2b45" stroke="#4a5a76" stroke-width="1.3"/>
<text x="549" y="545" fill="#e8edf7" font-size="13" font-family="Consolas, monospace">replay.py</text>
<text x="549" y="563" fill="#9fb0c8" font-size="11">回放引擎 · 286 行(QObject)</text>
<text x="549" y="563" fill="#9fb0c8" font-size="11">回放引擎 · 291 行(QObject)</text>
<text x="549" y="579" fill="#7dd3fc" font-size="11" font-family="Consolas, monospace">→ record 读取</text>
<rect x="856" y="524" width="296" height="56" rx="7" fill="#1d2b45" stroke="#4a5a76" stroke-width="1.3"/>
<text x="867" y="545" fill="#e8edf7" font-size="13" font-family="Consolas, monospace">calibration_service.py</text>
<text x="867" y="563" fill="#9fb0c8" font-size="11">标定 · 131 行</text>
<text x="867" y="563" fill="#9fb0c8" font-size="11">标定文件与归属 · 194 行</text>
<text x="867" y="579" fill="#7dd3fc" font-size="11" font-family="Consolas, monospace">无 Qt</text>
<rect x="1174" y="524" width="296" height="56" rx="7" fill="#152b25" stroke="#4a5a76" stroke-width="1.3"/>
<text x="1185" y="545" fill="#e8edf7" font-size="13" font-family="Consolas, monospace">scene3d/coordinator.py</text>
<text x="1185" y="563" fill="#9fb0c8" font-size="11">调度 · 560 行 · 无 Qt</text>
<text x="1185" y="563" fill="#9fb0c8" font-size="11">调度 · 619 行 · 无 Qt</text>
<text x="1185" y="579" fill="#7dd3fc" font-size="11" font-family="Consolas, monospace">mailbox + epoch</text>
<rect x="220" y="594" width="296" height="56" rx="7" fill="#152b25" stroke="#4a5a76" stroke-width="1.3"/>
<text x="231" y="615" fill="#e8edf7" font-size="13" font-family="Consolas, monospace">scene3d/camera.py</text>
<text x="231" y="633" fill="#9fb0c8" font-size="11">相机 · 210 行 · 无 Qt</text>
<text x="231" y="633" fill="#9fb0c8" font-size="11">相机 · 238 行 · 无 Qt</text>
<rect x="538" y="594" width="296" height="56" rx="7" fill="#1d2b45" stroke="#4a5a76" stroke-width="1.3"/>
<text x="549" y="615" fill="#e8edf7" font-size="13" font-family="Consolas, monospace">scene3d/view.py</text>
<text x="549" y="633" fill="#9fb0c8" font-size="11">3D 唯一入口 · 758 行</text>
<text x="549" y="633" fill="#9fb0c8" font-size="11">3D 唯一入口 · 757 行</text>
<text x="549" y="649" fill="#7dd3fc" font-size="11" font-family="Consolas, monospace">describe() / render_totals()</text>
<rect x="856" y="594" width="296" height="56" rx="7" fill="#1d2b45" stroke="#4a5a76" stroke-width="1.3"/>
<text x="867" y="615" fill="#e8edf7" font-size="13" font-family="Consolas, monospace">scene3d/asset_source.py</text>
<text x="867" y="633" fill="#9fb0c8" font-size="11">加载线程 · 257 行</text>
<text x="867" y="633" fill="#9fb0c8" font-size="11">加载线程 · 256 行</text>
<text x="867" y="649" fill="#7dd3fc" font-size="11" font-family="Consolas, monospace">验签 + 设备几何校验</text>
<rect x="1174" y="594" width="296" height="56" rx="7" fill="#1e273a" stroke="#fbbf24" stroke-width="1.3" stroke-dasharray="4 3"/>
<text x="1185" y="615" fill="#e8edf7" font-size="13" font-family="Consolas, monospace">scene3d/gl_backend.py</text>
<text x="1185" y="633" fill="#9fb0c8" font-size="11">GL 惰性 · 871 行</text>
<text x="1185" y="633" fill="#9fb0c8" font-size="11">GL 惰性 · 870 行</text>
<rect x="220" y="744" width="406" height="54" rx="7" fill="#152b25" stroke="#4a5a76" stroke-width="1.3"/>
<text x="231" y="765" fill="#e8edf7" font-size="13" font-family="Consolas, monospace">orisys_app_shared.scene3d</text>
<text x="231" y="783" fill="#9fb0c8" font-size="11">契约 / 采样 / 合成 / 包 / 签名 · 4057 行</text>
<text x="231" y="783" fill="#9fb0c8" font-size="11">契约 / 采样 / 合成 / 包 / 签名 · 4565 行</text>
<rect x="648" y="744" width="406" height="54" rx="7" fill="#152b25" stroke="#4a5a76" stroke-width="1.3"/>
<text x="659" y="765" fill="#e8edf7" font-size="13" font-family="Consolas, monospace">orisys_app_shared.acquisition</text>
<text x="659" y="783" fill="#9fb0c8" font-size="11">worker / packet / sinks / 图像后端 · 2343 行</text>
<text x="659" y="783" fill="#9fb0c8" font-size="11">worker / packet / sinks / 图像后端 · 2627 行</text>
<rect x="1076" y="744" width="406" height="54" rx="7" fill="#152b25" stroke="#4a5a76" stroke-width="1.3"/>
<text x="1087" y="765" fill="#e8edf7" font-size="13" font-family="Consolas, monospace">orisys_app_shared.record</text>
<text x="1087" y="783" fill="#9fb0c8" font-size="11">录像读写 / CSV / session.json · 2673 行</text>
<text x="1087" y="783" fill="#9fb0c8" font-size="11">录像读写 / CSV / session.json · 3008 行</text>
<rect x="220" y="810" width="406" height="54" rx="7" fill="#3a2a18" stroke="#4a5a76" stroke-width="1.3"/>
<text x="231" y="831" fill="#e8edf7" font-size="13" font-family="Consolas, monospace">orisys_app_shared.force</text>
<text x="231" y="849" fill="#9fb0c8" font-size="11">力源 / 标定 / 滤波 · 2645 行</text>
<text x="231" y="849" fill="#9fb0c8" font-size="11">力源 / 标定 / 滤波 · 3057 行</text>
<rect x="648" y="810" width="406" height="54" rx="7" fill="#152b25" stroke="#4a5a76" stroke-width="1.3"/>
<text x="659" y="831" fill="#e8edf7" font-size="13" font-family="Consolas, monospace">camera · prefs · paths · i18n · theme · logging</text>
<text x="659" y="849" fill="#9fb0c8" font-size="11">基础设施 · 1268 行</text>
<text x="659" y="849" fill="#9fb0c8" font-size="11">基础设施 · 1507 行</text>
<text x="1076" y="828" fill="#fbbf24" font-size="11.5">⚠ 唯一的例外:force.calibration_io 在 import 时</text>
<text x="1076" y="846" fill="#fbbf24" font-size="11.5">  import orisys(力相关单测需要装 SDK)</text>
<rect x="220" y="984" width="616" height="44" rx="7" fill="#251d33" stroke="#7c5cbf" stroke-width="1.3"/>
<text x="231" y="1005" fill="#e8edf7" font-size="13" font-family="Consolas, monospace">shared/tests</text>
<text x="231" y="1023" fill="#9fb0c8" font-size="11">22 文件 · 5992 行 · 316 项</text>
<text x="231" y="1023" fill="#9fb0c8" font-size="11">22 文件 · 7226 行 · 334 项</text>
<rect x="860" y="984" width="616" height="44" rx="7" fill="#251d33" stroke="#7c5cbf" stroke-width="1.3"/>
<text x="871" y="1005" fill="#e8edf7" font-size="13" font-family="Consolas, monospace">apps/studio/tests</text>
<text x="871" y="1023" fill="#9fb0c8" font-size="11">10 文件 · 5641 行 · 240 项 + 真 GL 14 项</text>
<text x="871" y="1023" fill="#9fb0c8" font-size="11">15 文件 · 8413 行 · 310 项 + 真 GL 14 项</text>
<rect x="220" y="1036" width="616" height="44" rx="7" fill="#251d33" stroke="#7c5cbf" stroke-width="1.3"/>
<text x="231" y="1057" fill="#e8edf7" font-size="13" font-family="Consolas, monospace">tools/ui_check.py</text>
<text x="231" y="1075" fill="#9fb0c8" font-size="11">3168 行 · 587 项 UI 断言</text>
<text x="231" y="1075" fill="#9fb0c8" font-size="11">3401 行 · 587 项 UI 断言</text>
<rect x="860" y="1036" width="616" height="44" rx="7" fill="#251d33" stroke="#7c5cbf" stroke-width="1.3"/>
<text x="871" y="1057" fill="#e8edf7" font-size="13" font-family="Consolas, monospace">tools/bench_scene.py · soak_scene.py · probe_gl.py</text>
<text x="871" y="1075" fill="#9fb0c8" font-size="11">性能 / 长时 / 平台报告</text>
@@ -135,7 +143,8 @@
<text x="314" y="949" fill="#9fb0c8" font-size="11.5">验证:单测 · 真 GL · UI 检查 · 性能长时报告</text>
<line x1="880" y1="930" x2="880" y2="950" stroke="#7dd3fc" stroke-width="2.2"/>
<path d="M 873 948 L 880 960 L 887 948 Z" fill="#7dd3fc"/>
<text x="40" y="1128" fill="#9fb0c8" font-size="11.5">依赖方向只有向下:扫全部 89 个模块(studio 32 + shared 57),模块级环 0 个;新增模块只要不反向 import 就不会破坏分层。</text>
<text x="40" y="1128" fill="#9fb0c8" font-size="11.5">依赖方向只有向下:扫全部 94 个模块(studio 37 + shared 57),模块级环 0 个;新增模块只要不反向 import 就不会破坏分层。</text>
<text x="40" y="1146" fill="#9fb0c8" font-size="11.5">2D 与 3D 是两条独立流水线,共同上游只有同一个 SdkPacket;任一失败只降级自己(2D/记录/曲线不受影响)。</text>
<text x="40" y="1164" fill="#9fb0c8" font-size="11.5">最贵的一处是 app_window.py(3044 行 / fan-out 36):视图装配、会话生命周期、业务决策、持久化四个角色目前都在它里面。</text>
<text x="40" y="1164" fill="#9fb0c8" font-size="11.5">app_window 已按职责拆开(会话 / 回放 / 布局 / 显示偏好),158 个方法里约 40 个是给测试与 ui_check 的一层转发;</text>
<text x="40" y="1182" fill="#9fb0c8" font-size="11.5">剩下的胶水是预览(_start_preview/_stop_preview)与 2D 渲染(_show_image_frame/_depth_range),它们同时属于面板与相机。</text>
</svg>

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 15 KiB