433b76e43c
把丢失的 box 录像放回 apps/studio/tests/data/,修掉第 3 节 3–10 号与两处过时测试桩, 注册 timing 标记并让根目录收集零错误;新增 tools/gate.ps1 与可选的 pre-push 钩子。 验证:studio 524 通过 19 跳过、shared 467 通过 18 跳过、ui_check 632 项 0 失败、gate.ps1 退出码 0。 细节见 docs/test-repair-plan.zh.md。
327 lines
21 KiB
Markdown
327 lines
21 KiB
Markdown
# Studio 软件架构(测量版)
|
||
|
||
本文不是"理想架构",而是**当前代码的真实依赖结构**:所有框图、数字与结论都由下面的脚本从源码里扫出来,
|
||
每条判断后面都跟着它的证据。配套的矢量图是 `studio-architecture.svg`(浏览器直接打开)。
|
||
|
||
生成方式(可复现,改完代码重跑即可):
|
||
|
||
> **脚本已不在工作区。** 下面两条命令指向 `D:\orisys_ws\.repro\` 下的一次性扫描脚本,该目录已清理
|
||
> (2026-09-27 工作区整理),扫描脚本本身没有保留。**本文的框图与数字仍然有效** —— 矢量图
|
||
> `studio-architecture.svg` 和 `studio-architecture.png` 已入库,是本文配套的、保留至今的产物。
|
||
> 如需重跑,按下面的 `PYTHONPATH` 与两条命令的口径重写等价脚本即可;结论口径见下方"统计口径"。
|
||
|
||
```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,哪些是惰性 ← 脚本已删除
|
||
```
|
||
|
||
统计口径:行数含空行;`fan-in` = 有多少模块 import 它;`fan-out` = 它 import 了多少模块(同一模块多次
|
||
import 只算一条边)。
|
||
|
||
---
|
||
|
||
## 1. 分层视图(真实依赖方向)
|
||
|
||
```mermaid
|
||
graph TB
|
||
subgraph L0["① 入口"]
|
||
MAIN["main.py<br/>109 行 · QApplication + 主题 + 首选项"]
|
||
end
|
||
subgraph L1["② 窗口外壳(只有它能看见全部 UI)"]
|
||
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 行"]
|
||
PLOTS["ui/force_plots<br/>三轴曲线 · 1243 行"]
|
||
CAL["ui/cal_panel<br/>标定表 · 793 行"]
|
||
DLG["sensor_dialog · plot_settings_dialog · cal_point_dialog"]
|
||
end
|
||
subgraph L3["④ Studio 领域服务(无 Qt 的与有 Qt 的分开)"]
|
||
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/>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
|
||
WIN --> CAL
|
||
WIN --> DLG
|
||
MESH --> VIEW
|
||
VIEW --> COORD
|
||
VIEW --> ASSET
|
||
VIEW --> GLB
|
||
VIEW --> CAM
|
||
COORD --> S3D
|
||
VIEW --> S3D
|
||
ASSET --> S3D
|
||
WIN --> REC
|
||
WIN --> REP
|
||
WIN --> CALSVC
|
||
REC --> RC
|
||
REP --> RC
|
||
CALSVC --> FORCE
|
||
WIN --> AQ
|
||
WIN --> FORCE
|
||
WIN --> RC
|
||
PLOTS --> FORCE
|
||
CAL --> FORCE
|
||
ARROW --> AQ
|
||
MESH --> S3D
|
||
```
|
||
|
||
纯文本版(无 Mermaid 渲染器时看这个):
|
||
|
||
```
|
||
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
|
||
mesh3d_panel → scene3d/view (3D 的唯一入口)
|
||
view → { coordinator(无 Qt) , asset_source(线程) , gl_backend(惰性 GL) , camera(无 Qt) , widget }
|
||
coordinator → composer(共享) → contracts(共享)
|
||
```
|
||
|
||
## 2. 运行时与线程视图
|
||
|
||
```mermaid
|
||
graph LR
|
||
subgraph GUI["GUI 线程(Qt 事件循环)"]
|
||
W["StudioMainWindow"]
|
||
T1["运行定时器 16 ms<br/>_on_run_tick"]
|
||
T2["回放定时器 33 ms<br/>_on_play_tick"]
|
||
T3["预览定时器"]
|
||
PANELS["面板:2D / 3D / 曲线 / 标定"]
|
||
end
|
||
subgraph ACQ["采集线程(每会话一条)"]
|
||
FW["FingerAcquisitionWorker / BoxWorker"]
|
||
PKT["SdkPacket"]
|
||
end
|
||
subgraph RECW["记录写线程"]
|
||
EXP["ExperimentRecorder<br/>视频 + forces.csv + session.json"]
|
||
end
|
||
subgraph S3DW["三维合成线程(1 条)"]
|
||
CMP["SceneComposer.compose"]
|
||
end
|
||
subgraph S3DA["场景加载线程(1 条)"]
|
||
LOAD["SceneAssetLoader<br/>读包 + 验签 + 设备几何校验"]
|
||
end
|
||
FW --> PKT
|
||
PKT -->|记录 sink| EXP
|
||
PKT -->|UI sink<br/>submit_packet| W
|
||
W -->|ImageFrame→image_backend| PANELS
|
||
W -->|"submit(容量 1 mailbox,尝试不阻塞)"| CMP
|
||
CMP -->|结果在 GUI 线程校验 epoch| W
|
||
LOAD -->|loaded 信号| W
|
||
T1 --> W
|
||
T2 --> W
|
||
T3 --> W
|
||
W --> PANELS
|
||
```
|
||
|
||
关键性质(都有测试钉住):
|
||
|
||
- **采集线程永不等待显示**:`submit` 只做身份/形状的廉价校验,容量 1 的 mailbox 由后来的帧替换前者;
|
||
生产者还会在"已有一帧在等"时干脆不做那次 O(H×W) 的拷贝(`skipped_producer`)。
|
||
- **合成线程与加载线程各只有 1 条**,且同时最多一次合成在飞行;结果必须通过 `epoch` + 身份(source /
|
||
session / generation / request_id)校验才会被 GUI 线程接受。
|
||
- **记录通道与显示通道共用同一个 packet,但计数完全分开**:显示丢帧(`superseded`)永不进记录。
|
||
|
||
## 3. 二维 / 三维显示子系统(最近两轮重构后的样子)
|
||
|
||
```mermaid
|
||
graph TB
|
||
subgraph SRC["生产者(4 条)"]
|
||
LIVE["实时 packet.processed_frame + flow + depth"]
|
||
RECOMP["重算:同一采集链跑在录像上"]
|
||
REPLAY["回放:ReplayEngine.video_frame_bgr() 原始帧"]
|
||
PREV["预览:摄像头原始帧"]
|
||
end
|
||
IB["image_backend.py(共享)<br/>ImageFrame → plan_image() → render_image()"]
|
||
PANEL2D["ui/arrow_panel<br/>模式集/注解来自 plan"]
|
||
AP["packet → adapters → coordinator(mailbox+epoch)→ SceneComposer(线程)"]
|
||
VIEW3D["scene3d/view<br/>describe() / render_totals() / capabilities()"]
|
||
PANEL3D["scene3d/panel(参数+图例+色条)"]
|
||
GLW["scene3d/widget → gl_backend(GL 惰性)"]
|
||
LIVE --> IB
|
||
RECOMP --> IB
|
||
REPLAY --> IB
|
||
PREV --> IB
|
||
IB --> PANEL2D
|
||
LIVE --> AP
|
||
RECOMP --> AP
|
||
AP --> VIEW3D
|
||
VIEW3D --> GLW
|
||
VIEW3D --> PANEL3D
|
||
```
|
||
|
||
两条显示路径**互不干扰**:2D 是一条纯数据流水线(`ImageFrame → 一个渲染函数 → 一个 QImage`),
|
||
3D 是"契约 + 合成线程 + GL"的另一条。它们的共同上游只是同一个 `SdkPacket`。
|
||
|
||
## 4. 边界规则与证据
|
||
|
||
| 规则 | 是否成立 | 证据 |
|
||
|---|---|---|
|
||
| 模块级无循环依赖 | ✅ | 扫描 **94 个模块**(studio 37 + shared 57)的 import:**环 0 个** |
|
||
| 共享层不依赖 Qt | ✅(有一条惰性例外) | `import` 时拉进 PySide6 的模块**全部**在 `orisys_studio.*`;`shared/theme/qss.py` 用 PySide6 **画图标时**才 import(函数内 + try 保护)。两个 app 的 `test_studio_binding_purity.py`/`test_binding_purity.py` 现在静态强制"只允许 PySide6"(在此之前 `qss.py` 的 import 因为写的是 PyQt5 而静默失败,AutoCal 的箭头图标一直没有画出来) |
|
||
| OpenGL 从不在 import 时拉入 | ✅ | 唯一使用者 `scene3d/gl_backend.py` 是函数内惰性 import;`widget.py` 同理 |
|
||
| 三维调度与相机无 Qt,可脱离 GUI 测试 | ✅ | 新解释器里 `import orisys_studio.scene3d.coordinator` / `.camera` → 不含 PySide6/OpenGL/SDK |
|
||
| 采集侧可在无 GUI 环境导入 | ✅ | `acquisition.arrows` / `.sinks`、`record.playback`、`scene3d.contracts`、`scene3d.composer` 全部"干净" |
|
||
| 共享层与 SDK 解耦 | ⚠️ **不成立** | `orisys_app_shared.force.calibration_io` 在 import 时 `import orisys`;于是 `import orisys_app_shared.force.history` 也会把 SDK 拉进来 |
|
||
| 三维是单向流动 | ✅ | `window → mesh3d_panel → view → {coordinator, composer, widget, gl_backend, camera}`;`coordinator` 不知道 Qt/GL/IO |
|
||
| 契约集中在一处 | ✅ | `orisys_app_shared.scene3d.contracts`:1185 行、**fan-in 22**(全仓最高),fan-out 0(只依赖 numpy) |
|
||
|
||
## 5. 指标
|
||
|
||
**改动最贵的地方(行数 × 扇出)**
|
||
|
||
| 模块 | 行数 | fan-out | 说明 |
|
||
|---|---:|---:|---|
|
||
| `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 文件 / 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`(生产,37 模块) | 14 655 | — |
|
||
| `shared`(生产,57 模块,无 Qt) | 14 764 | 4 565 |
|
||
| 测试(shared 7226 + studio 8413) | 15 639 | — |
|
||
| 工具(ui_check 3401 + scene_assets 工具 645) | 4 046 | — |
|
||
|
||
即**验证面(19.7k 行)比 studio 的生产代码(14.7k 行)还大**。共享层里记录 3008、力 3057、采集 2627 行,
|
||
三维 4565 行——三维不是附属功能,它和"记录/力"是同一量级的子系统。
|
||
|
||
## 6. 评估:合理与不合理的地方
|
||
|
||
### 已经合理、值得保住的
|
||
|
||
1. **依赖无环 + 分层单向**(扫描证据)。这不是小事:有环的项目才会出现"改 A 必须改 B 再改回来"的
|
||
连锁;这里没有。新增模块时只要不反向 import 就不会破坏。
|
||
2. **共享层与 UI 的物理隔离**:`shared` 里 `import` 时不拉 Qt/GL,采集线程、CI、离线工具都能在无 GUI
|
||
环境跑;`contracts` 甚至只依赖 numpy。
|
||
3. **三维子系统的接口是显式的**:`describe()`(我是什么)/`render_totals()`(显示了什么)/
|
||
`capabilities()`(数据源能给什么)/`set_plan()`(这一帧能画什么)。窗口只调这四个,不碰渲染细节——
|
||
这几轮重构主要就是在把隐式耦合换成这四条缝。
|
||
4. **降级路径是设计出来的,不是碰巧**:无 GL / 无资产 / 几何不符 / 字段失效 / 场景被平台锁拒绝,
|
||
每一种都只降级三维或某个面板,2D 与记录不受影响(有专门测试)。
|
||
5. **危险的知识点集中在有测试的地方**:单位与坐标系(`legacy.py` + Procrustes 残差)、包格式与签名
|
||
(`package.py`/`protect.py`)、采样无效语义(`sampling.py`)——这些最容易静默出错的部分都有
|
||
输入样例 + 失败样例。
|
||
|
||
### 不合理、会随时间变贵的地方(按严重程度)
|
||
|
||
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`(录像读取)。
|
||
3. **两套 `scene3d` 包**:`orisys_app_shared.scene3d`(契约/采样/合成/包,无 Qt)与
|
||
`orisys_studio.scene3d`(coordinator/view/panel/widget/GL)。边界理由是"Qt/GL 不进共享层",
|
||
但 `coordinator` 其实无 Qt(已实测),放在 studio 只是因为"它服务 GUI 计时器"——这条理由不硬。
|
||
4. **共享层 `force` 硬依赖 SDK**:`calibration_io` 在 import 时 `import orisys`,于是"共享层可以脱离
|
||
SDK 做单元测试"这条**只对三维成立,对力不成立**(力相关的测试必须装 SDK)。这也解释了为什么
|
||
离线工具与力曲线测试总是整套跑。
|
||
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. **验证面 > 生产面**: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`。
|
||
|
||
---
|
||
|
||
_本文与图由源码扫描生成;扫描脚本 `.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`)。
|
||
数字口径:行数含空行,`fan-out` = 该模块 import 了多少个模块。_
|
||
|
||
_生成时间:拆分 `app_window`(A1/A2 + B + C1 + C2 + 标定收尾 + 清零确认修复)之后。_
|