Files
orisys-app/docs/architecture/studio-architecture.svg
T
ykcheungab dd0ee2c71e docs(architecture): Studio 架构图与可维护性评估(按源码实际 import 关系生成)
- What:
  1. `docs/architecture/studio-architecture.md`:分层视图(Mermaid + 纯文本版)、运行时/线程视图、
     二维/三维显示子系统视图、边界规则与证据表、指标表(模块行数 × 扇出、稳定核心 fan-in、验证面规模)、
     可维护性评估(5 条优点 / 7 条风险,逐条带证据)、6 条建议(按性价比排序)。
  2. `docs/architecture/studio-architecture.svg`(浏览器直接打开)与同名 `.png`(由 SVG 经 QtSvg 栅格化,
     给没有 SVG 渲染器的场景):分层海报,层间流动用箭头表示,跨模块关系写在框内。
  3. 数据来源(在 `.repro/`,可重跑):`studio_arch_scan.py` 扫 89 个模块的 import 图并给出 fan-in/fan-out
     与**模块级环**;`studio_arch_imports.py` 分别统计"import 时就拉 Qt/GL/SDK"与"函数内惰性 import",
     并在**新解释器**里逐个验证"导入 X 是否会拉进 PyQt5/OpenGL/SDK";`make_studio_svg.py`/`svg_to_png.py`
     生成图。原始数据 `studio_arch.json`、`studio_arch_imports.json`。
- Why: 用户要看架构图来判断"软件组织是否合理、是否容易维护"。因此所有框与数字都从源码实际 import 关系
  生成,而不是画一个理想分层:报告里同时给出**成立**的边界规则(无环、共享层 import 时不依赖 Qt/GL、
  三维调度与相机无 Qt 可脱离 GUI 测试)与**不成立**的一条(`force/calibration_io` 在 import 时 import
  orisys,于是力相关单测必须装 SDK),以及最贵的单点 `app_window.py`(3044 行 / 158 方法 / fan-out 36)。
- Verification:
  · 扫描结果:89 个模块(studio 32 + shared 57)模块级**环 0 个**;import 时拉 PyQt5 的 21 个模块**全部**
  在 `orisys_studio.*`;OpenGL 只在 `gl_backend`/`widget` 函数内惰性加载;新解释器验证
  `scene3d.contracts/composer`、`acquisition.arrows/sinks`、`record.playback`、`theme`、
  `studio.scene3d.coordinator/camera` 均为"干净"(不含 PyQt5/OpenGL/SDK),而 `force.history` 会拉进 SDK。
  · 规模:studio src 13 376 行(三维 4 326)、shared 14 820 行(三维 4 576 / 记录 2 673 / 力 2 645 /
  采集 2 343)、测试 11 633 行、工具 5 272 行;`scene3d.contracts` fan-in 22 为全仓最高。
  · 图的自检:SVG 由 `ET.parse` 验证为良构 XML,并栅格化成 PNG 后逐版目视检查过(第一版箭头穿框,已改为
  层间箭头 + 框内关系文字)。
- Not in this change: 不改任何产品代码;建议 1(拆 `app_window`)等 6 条尚未实施,它们属于下一轮工作,
  每条都写了收益、成本与回归门。
2026-09-15 08:25:20 +08:00

141 lines
14 KiB
XML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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"/>
<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"/>
<text x="46" y="111" fill="#7dd3fc" font-size="15.5" font-weight="600">① 入口</text>
<rect x="30" y="176" width="1500" height="116" rx="10" fill="#161d2c" stroke="#27324a" stroke-width="1"/>
<text x="46" y="203" fill="#7dd3fc" font-size="15.5" font-weight="600">② 窗口外壳</text>
<text x="46" y="223" fill="#9fb0c8" font-size="11.5">只有它能看见全部 UI</text>
<rect x="30" y="322" width="1500" height="148" rx="10" fill="#161d2c" stroke="#27324a" stroke-width="1"/>
<text x="46" y="349" fill="#7dd3fc" font-size="15.5" font-weight="600">③ 面板</text>
<text x="46" y="369" fill="#9fb0c8" font-size="11.5">各自的画布与控件</text>
<rect x="30" y="500" width="1500" height="190" rx="10" fill="#161d2c" stroke="#27324a" stroke-width="1"/>
<text x="46" y="527" fill="#7dd3fc" font-size="15.5" font-weight="600">④ Studio 领域服务</text>
<text x="46" y="547" fill="#9fb0c8" font-size="11.5">有 Qt 的与无 Qt 的分开</text>
<rect x="30" y="720" width="1500" height="210" rx="10" fill="#161d2c" stroke="#27324a" stroke-width="1"/>
<text x="46" y="747" fill="#7dd3fc" font-size="15.5" font-weight="600">⑤ 共享领域层</text>
<text x="46" y="767" fill="#9fb0c8" font-size="11.5">采集 / 记录 / 力 / 三维契约 —— import 时不拉 Qt</text>
<rect x="30" y="960" width="1500" height="150" rx="10" fill="#161d2c" stroke="#27324a" stroke-width="1"/>
<text x="46" y="987" fill="#7dd3fc" font-size="15.5" font-weight="600">⑥ 验证面</text>
<text x="46" y="1007" fill="#9fb0c8" font-size="11.5">为什么这个架构敢改</text>
<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"/>
<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>
<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="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="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="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="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>
<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="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>
<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="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>
<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="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="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>
<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>
<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>
<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>
<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>
<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="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>
<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>
<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>
<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>
<line x1="300" y1="146" x2="300" y2="166" stroke="#7dd3fc" stroke-width="2.2"/>
<path d="M 293 164 L 300 176 L 307 164 Z" fill="#7dd3fc"/>
<text x="314" y="165" fill="#9fb0c8" font-size="11.5">启动</text>
<line x1="300" y1="292" x2="300" y2="312" stroke="#7dd3fc" stroke-width="2.2"/>
<path d="M 293 310 L 300 322 L 307 310 Z" fill="#7dd3fc"/>
<text x="314" y="311" fill="#9fb0c8" font-size="11.5">装配窗口 + 面板 + 信号转发</text>
<line x1="880" y1="292" x2="880" y2="312" stroke="#7dd3fc" stroke-width="2.2"/>
<path d="M 873 310 L 880 322 L 887 310 Z" fill="#7dd3fc"/>
<line x1="300" y1="470" x2="300" y2="490" stroke="#7dd3fc" stroke-width="2.2"/>
<path d="M 293 488 L 300 500 L 307 488 Z" fill="#7dd3fc"/>
<text x="314" y="489" fill="#9fb0c8" font-size="11.5">领域服务:会话 / 记录 / 回放 / 标定</text>
<line x1="880" y1="470" x2="880" y2="490" stroke="#7dd3fc" stroke-width="2.2"/>
<path d="M 873 488 L 880 500 L 887 488 Z" fill="#7dd3fc"/>
<text x="894" y="489" fill="#9fb0c8" font-size="11.5">三维:view → coordinator / GL / 加载线程</text>
<line x1="300" y1="690" x2="300" y2="710" stroke="#7dd3fc" stroke-width="2.2"/>
<path d="M 293 708 L 300 720 L 307 708 Z" fill="#7dd3fc"/>
<text x="314" y="709" fill="#9fb0c8" font-size="11.5">契约 · packet · mailbox(无 Qt 的共享层)</text>
<line x1="880" y1="690" x2="880" y2="710" stroke="#7dd3fc" stroke-width="2.2"/>
<path d="M 873 708 L 880 720 L 887 708 Z" fill="#7dd3fc"/>
<line x1="300" y1="930" x2="300" y2="950" stroke="#7dd3fc" stroke-width="2.2"/>
<path d="M 293 948 L 300 960 L 307 948 Z" fill="#7dd3fc"/>
<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="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>
</svg>