diff --git a/docs/architecture/studio-architecture.md b/docs/architecture/studio-architecture.md
new file mode 100644
index 0000000..73f6077
--- /dev/null
+++ b/docs/architecture/studio-architecture.md
@@ -0,0 +1,280 @@
+# Studio 软件架构(测量版)
+
+本文不是"理想架构",而是**当前代码的真实依赖结构**:所有框图、数字与结论都由下面的脚本从源码里扫出来,
+每条判断后面都跟着它的证据。配套的矢量图是 `studio-architecture.svg`(浏览器直接打开)。
+
+生成方式(可复现,改完代码重跑即可):
+
+```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
110 行 · QApplication + 主题 + 首选项"]
+ end
+ subgraph L1["② 窗口外壳(只有它能看见全部 UI)"]
+ WIN["app_window.py · StudioMainWindow
3044 行 · 158 个方法 · fan-out 36"]
+ BAR["brand_bar · toolbar · record_status"]
+ end
+ subgraph L2["③ 面板(各自的画布与控件)"]
+ ARROW["ui/arrow_panel
2D 图像 · 519 行"]
+ MESH["ui/mesh3d_panel
3D 宿主容器 · 405 行"]
+ PLOTS["ui/force_plots
三轴曲线 · 1243 行"]
+ CAL["ui/cal_panel
标定表 · 793 行"]
+ DLG["sensor_dialog · plot_settings_dialog · cal_point_dialog"]
+ end
+ subgraph L3["④ Studio 领域服务(无 Qt 的与有 Qt 的分开)"]
+ REC["recording.py
记录协调 · 268 行"]
+ REP["replay.py
回放引擎 · 286 行(QObject)"]
+ CALSVC["calibration_service.py
131 行"]
+ COORD["scene3d/coordinator.py
560 行 · **无 Qt**"]
+ CAM["scene3d/camera.py
210 行 · **无 Qt**"]
+ VIEW["scene3d/view.py
758 行 · 3D 唯一入口"]
+ ASSET["scene3d/asset_source.py
257 行 · 场景加载线程"]
+ GLB["scene3d/gl_backend.py
871 行 · **GL 只在运行时惰性 import**"]
+ end
+ subgraph L4["⑤ 共享领域层(采集/记录/力/三维契约,无 Qt 依赖)"]
+ AQ["acquisition
2343 行 · worker / packet / sinks / 图像后端"]
+ RC["record
2673 行 · 录像读写 / CSV / session.json"]
+ FORCE["force
2645 行 · 力曲线源 / 标定 / 滤波"]
+ S3D["scene3d
4057 行 · 契约 / 采样 / 合成 / 包格式 / 签名"]
+ MISC["camera · prefs · paths · i18n · theme · logging"]
+ end
+ MAIN --> WIN
+ WIN --> BAR
+ 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
+ ├─ 面板: 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
_on_run_tick"]
+ T2["回放定时器 33 ms
_on_play_tick"]
+ T3["预览定时器"]
+ PANELS["面板:2D / 3D / 曲线 / 标定"]
+ end
+ subgraph ACQ["采集线程(每会话一条)"]
+ FW["FingerAcquisitionWorker / BoxWorker"]
+ PKT["SdkPacket"]
+ end
+ subgraph RECW["记录写线程"]
+ EXP["ExperimentRecorder
视频 + forces.csv + session.json"]
+ end
+ subgraph S3DW["三维合成线程(1 条)"]
+ CMP["SceneComposer.compose"]
+ end
+ subgraph S3DA["场景加载线程(1 条)"]
+ LOAD["SceneAssetLoader
读包 + 验签 + 设备几何校验"]
+ end
+ FW --> PKT
+ PKT -->|记录 sink| EXP
+ PKT -->|UI sink
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["回放:ReplayController.video_frame_bgr() 原始帧"]
+ PREV["预览:摄像头原始帧"]
+ end
+ IB["image_backend.py(共享)
ImageFrame → plan_image() → render_image()"]
+ PANEL2D["ui/arrow_panel
模式集/注解来自 plan"]
+ AP["packet → adapters → coordinator(mailbox+epoch)→ SceneComposer(线程)"]
+ VIEW3D["scene3d/view
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. 边界规则与证据
+
+| 规则 | 是否成立 | 证据 |
+|---|---|---|
+| 模块级无循环依赖 | ✅ | 扫描 **89 个模块**(studio 32 + 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 |
+| 采集侧可在无 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` | 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 的唯一入口(刻意的) |
+
+**稳定核心(被依赖最多)**:`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。
+
+**生产面 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 | — |
+
+即**验证面(16.9k 行)比 studio 的生产代码(13.4k 行)还大**。共享层里记录 2673、力 2645、采集 2343 行,
+三维 4576 行——三维不是附属功能,它和"记录/力"是同一量级的子系统。
+
+## 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 生命周期控制器 ③业务决策(单位回退、量程、重算/回放互斥、面板尺寸)
+ ④持久化与诊断聚合。后果:任何功能改动都要动它(本次两轮修 bug 里,`_load_replay_video`、
+ `_sync_replay_actions`、`_update_run_enabled`、`_render_*` 全在同一个文件里互相影响);
+ 它是**唯一** fan-in=1 的中心,也就是说没有第二个人能独立改它的任一部分而不碰冲突热点。
+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` 3168 行单文件**:它是产品 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` 里的字符串/布局断言。
+
+### 一句话结论
+
+**组织是合理的、容易维护的**(无环、分层单向、共享层无 Qt、危险知识集中在有测试的地方),
+**但有一个明确的单点:`app_window.py`**。它现在是"能维护"和"改一处要被它绊住"之间的分界线;
+再往上加功能(下一个面板、下一种传感器、下一个回放能力)之前,把它拆开是性价比最高的一件事。
+
+## 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` 只应有移动与转发 |
+| 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/` 一层小适配器 | 内部重构不再一次性打碎几十个测试 | 小-中 | 全套 |
+
+不建议做的事:**不要**为了"看起来分层漂亮"把 `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`。_
diff --git a/docs/architecture/studio-architecture.png b/docs/architecture/studio-architecture.png
new file mode 100644
index 0000000..1b3aad9
Binary files /dev/null and b/docs/architecture/studio-architecture.png differ
diff --git a/docs/architecture/studio-architecture.svg b/docs/architecture/studio-architecture.svg
new file mode 100644
index 0000000..4b97296
--- /dev/null
+++ b/docs/architecture/studio-architecture.svg
@@ -0,0 +1,141 @@
+
\ No newline at end of file