docs: C ABI migration campaign plans, handoffs and roadmap

Complete documentation set for the facade migration (B1-R6) and beyond:
facade-migration-roadmap, handoffs v3-v6, R5 guides, R6 cleanup plan,
R7 pure-ABI plan, long-term plans (riir/ai-agent/gtest/ui-redesign),
updated Chinese README draft, UI design mockups, and the Google Test +
struct-typedef rules in CONTRIBUTING.
This commit is contained in:
2026-07-26 22:42:44 +08:00
parent fcf717f6a7
commit c486c853ff
22 changed files with 3482 additions and 4 deletions
+27
View File
@@ -0,0 +1,27 @@
# 长期计划(plans)
本目录收纳 Oak 的**中长期规划文档与并行执行计划**。当前正在进行的
**C ABI 迁移战役**的文档在上一层(`docs/zh/`),见下方"当前进行中"。
## 目录
| 文档 | 内容 | 启动前提 |
|---|---|---|
| [`riir.md`](riir.md) | **RIIR 绞杀者模式执行计划**:C ABI 迁移完成后,把 liboakengine 安全拆成若干小模块,再逐个用 Rust 重写;含 API 冻结保证、模块图、六步流程与验证门禁 | C ABI 迁移战役验收完成 |
| [`ai-agent-design.md`](ai-agent-design.md) | **AI Agent 设计**:多模态 LLM 经 MCP 调用策展工具面自动剪辑,渲染帧回喂形成"编辑→看图→再编辑"视觉闭环;含工具面、回放回路、安全与测试 | RIIR 拆分完成(面对一堆小库) |
| [`gtest-migration-guide.md`](gtest-migration-guide.md) | **测试统一到 Google Test**:把 OAK_ADD_TEST 宏框架、纯 C assert、已有 gtest 三套收敛为单一 Google Test,ctest 仅作运行器 | R5 验收完成后启动(可与 UI 改版并行) |
| [`ui-redesign-plan.md`](ui-redesign-plan.md) | **主界面 UI 改版**:依据 `design/` 三张设计图落地 10 个工作包(工具条、双监看、效果栈检查器、节点编辑器移位、电平条、状态栏等),全部文字精确定义 | R5 验收完成后启动(可与 GTest 迁移并行) |
## 当前进行中(不在本目录)
C ABI 迁移战役的执行文档在 `docs/zh/`:
- `c-abi-migration-handoff.md`(v3 交接)、`c-abi-migration-handoff-v4.md`(v4 重做计划)
- `facade-migration-roadmap.md`(批次记录)
- `r5-app-migration-guide.md`、`r5-phase2-detailed-guide.md`(R5 app 侧迁移指引)
## 其他参考
- 构建:`docs/zh/build.md`、`docs/zh/build_macos-zh.md`
- 工程文件:`docs/zh/project-file-reference.md`
- 代码风格与 Google Test 要求:`CONTRIBUTING.md`(仓库根)
+141
View File
@@ -0,0 +1,141 @@
# AI Agent 设计文档(RIIR 拆分后长期规划)
> 本文是 Oak 引入 AI 能力的长期设计,**执行前提是 RIIR 绞杀者拆分完成**
> (见 [`riir.md`](riir.md))。彼时 `liboakengine.so` 已不存在,取而代之的是
> 一组以纯 C ABI 为缝的小动态库。本文面向没有当前对话记忆的执行者,自包含。
>
> **一句话**:把多模态 LLM 当成引擎 C ABI 的**第三个一等消费者**
> (继 oak-cli、oak-render-worker 之后),用 MCP 暴露策展过的工具面,
> 用渲染管线把帧喂回给多模态模型,形成"编辑 → 看图 → 再编辑"的视觉闭环。
---
## 1. 定位与前提
### 1.1 前提(未满足不动工)
- RIIR 拆分战役完成:引擎已拆为 §2.1 的小库;各库导出仅 C 符号;
`oakengine_*` facade 壳稳定且全量测试绿。
- Google Test 已是唯一测试框架;`gtest_discover_tests` 已接入。
- 本文不改动 RIIR 既定的模块划分,只在模块化树上**新增叶子**。
### 1.2 设计铁律(继承自迁移/拆分战役)
1. AI Agent **只经 C ABI** 访问引擎,一行 engine C++ 都不碰;不污染符号边界。
2. Agent 的一切编辑动作**必须可撤销**(undoable 原语),UI 默认"确认后执行"。
3. 不为 AI 发明新的引擎内部机制;工具面是现有 facade/小库的组合。
4. 引擎各模块**不得新增 Qt 依赖、不得新增 QObject 信号/moc 类**。
## 2. 总体架构
### 2.1 在模块化树上的位置
```
app / oak-cli / oak-render-worker / oak-agent / oak-mcp-server
│
liboakengine-facade(壳:capi + 事件 + init)
│
┌────────┬────────┼─────────┬──────────┐
oaktask oakrender oakplugin oakaudio oakserialize
│ │ │ │ │
└────────┴────┬───┴──────────┴──────────┘
│
oakmodel(节点图 + 项目模型 + 时间线模型)
│
┌────────┼─────────┐
oakcodec liboakcore oakbackend(GPU 插件)
│
ffmpeg_bridge
```
**新增三个叶子组件**(与 oak-cli、oak-render-worker 平级,都是纯消费者):
- **`oak-mcp-server`**:把策展过的工具面经 MCP 暴露给任何 LLM 客户端。
- **`oak-agent`**:无头 Agent 运行时(对话编排 + 视觉闭环),供脚本/CI/本地使用。
- **editor AI 面板**:app 内的聊天/操作日志/确认界面,与 `oak-agent` 复用同一工具面。
AI 功能**不进入** oakmodel、oakrender 等引擎模块,引擎核心对 AI 无感知。
### 2.2 视觉闭环(本设计的核心)
```
多模态 LLM ──► oak-agent ──► facade/小库执行编辑 ──► 渲染取帧 ──► PNG ──► 回喂 LLM
▲ │
└──────────────── 看图判断(效果/切点/内容定位) ◄───────────────┘
```
- **验证式**:每次编辑后取一帧,LLM 判断"效果对不对"。
- **内容感知式**:沿时间线批量取缩略图拼 contact sheet,LLM 扫图定位
("人何时进画面""哪里该切"),Agent 据此下刀——自动粗剪/打点的雏形。
- **连续回放**:经 playback 族起范围播放,按间隔采样帧。
## 3. 工具面(策展,非全量 facade)
**不暴露全部 ~200+ facade 函数**,而是策展约 25 个高层工具,每个是 facade/小库
的组合。`oak-mcp-server` 内部就是一个薄模块,链接 facade 壳与相关小库。
| 工具 | 落到哪个库 | 说明 |
|---|---|---|
| `create_project` / `open_project` / `save_project` | facade 壳 + oakmodel | 工程生命周期 |
| `probe_media` / `import_footage` / `get_media_info` | oakcodec + facade 壳 | 媒体探测与导入 |
| `add_track` / `add_clip` / `trim_clip` / `ripple` / `add_transition` / `add_marker` | oakmodel(经 facade 时间线族) | 时间线编辑 |
| `add_effect(effect_id)` / `set_param` / `set_keyframe` | oakmodel(经 facade node 族) | 节点与关键帧 |
| `apply_lut` / `set_color_transform` | oakmodel + facade color 族 | 调色 |
| `get_frame(time)` / `get_thumbnails(range,n)` / `get_audio_levels` | oakrender + oakbackend | **视觉闭环的取帧口** |
| `export_render(params)` | oaktask + oakcodec | 导出 |
**取帧→PNG 通路**(视觉闭环关键路径):
`get_frame` 经 oakrender 的预览请求得到 RGBA 帧(POD:`宽/高/字节流`),
再经 oakcodec 的 OIIO 编码器出 PNG,base64 后作为图片消息发给 LLM。
缩略图用同一路径降采样,多张拼 contact sheet。
## 4. 协议:MCP(Model Context Protocol)
工具协议**定为 MCP**,理由:
- render-worker 已在用 **NDJSON over stdin/stdout 的 IPC**——MCP 本质是该模式
的标准化,实现路径一致。
- 暴露成 `oak-mcp-server` 后,**外部 LLM 客户端(Claude Desktop、各类 agent
框架)可直接连接复用**,无需自研对话编排。
- `oak-agent` 与 editor AI 面板都连同一个 MCP server,**一份工具面,多处消费**。
## 5. 模型层
抽象 `LLMProvider` 接口(输入:消息 + 图片;输出:文本 + tool_calls),两个后端:
- **云端**:Claude / GPT 多模态(效果优先)。
- **本地**:llama.cpp 跑 Qwen-VL / LLaVA 类多模态模型(隐私、离线优先)。
API key 只走环境变量,**绝不写入 config / 工程文件**。无 key 时优雅降级为
"仅本地工具"(仍可用 MCP,但不做对话编排)。
## 6. 安全
- **可撤销**:所有编辑走 undoable 原语;AI 面板提供"撤销整段会话"。
- **确认模式**:默认每次 Agent 动作需用户确认才 apply;可切换自动模式。
- **沙箱会话**:Agent 默认在临时工程中操作,用户接受后才落盘到真实工程。
- **资源**:取帧/扫描限帧率与分辨率上限,防止批量取帧拖垮渲染进程。
## 7. 可测试(与项目风格一致)
1. **Mock LLM server**:录制/回放 tool_call 序列与固定回复,让 Agent loop 在
CI 无 key 无网络跑通(Google Test)。
2. **黄金帧校验**:复用 render-worker 端到端 harness(真实渲染 ≥2 帧 +
像素非全黑 + 一致性断言),验证"Agent 的编辑确实改变了画面"。
3. **会话回放**:tool_call + 帧哈希落盘日志,可回放复现、可作测试夹具。
## 8. 里程碑(RIIR 完成后启动)
1. **M1 工具面**:`oak-mcp-server`(facade → MCP,~25 工具)+ 取帧→PNG 通路。
2. **M2 无头闭环**:Mock LLM + `oak-agent`,跑通"LLM→工具→取帧→回喂",
CI 可测(无网络)。
3. **M3 AI 面板**:editor 内聊天 + 操作日志 + 确认模式 + 撤销会话。
4. **M4 本地模型与内容感知**:llama.cpp provider、时间线扫描打点、自动粗剪。
## 9. 风险与边界(明确不做)
- **不**把 LLM/推理放进 oakmodel 或任何引擎模块(引擎对 AI 无感知)。
- **不**为 AI 绕过 C ABI 直接调 engine C++(边界不污染)。
- **不**把 API key 落盘到工程/config。
- **不**让取帧回路阻塞 GUI 线程(取帧走渲染/后台路径,UI marshal 回主线程)。
- 第三方大模型客户端的接入细节(OAuth、计费、配额)**超出本文范围**,按需另立文档。
+154
View File
@@ -0,0 +1,154 @@
# 测试统一到 Google Test — 迁移指引
> 本文指导把仓库里并存的三套测试框架统一收敛到 **Google Test**。
> 面向执行者(DeepSeek Flash 或任何接手代理),自包含,可直接照做。
> 工作分支:`c-abi-migration`。**启动前提:R5(C ABI app 侧迁移)验收完
> 成之后**(R5 期间测试是唯一的回归防线,不在迁移途中换测试框架);
> 启动后可与 UI 改版计划并行(文件域不相交)。每迁完一个测试二进制
> 立即提交,每步全量 ctest 绿才进下一步。
>
> **ctest 的定位**:统一后 ctest 仍然是唯一的测试**运行入口**
> (`ctest --output-on-failure`),Google Test 是唯一的测试**编写框架**。
> 两者不冲突——用 `gtest_discover_tests()` 让 ctest 按用例粒度发现 gtest 用例。
---
## 1. 现状:三套框架并存
| 框架 | 位置 | 编写方式 | 构建/注册 |
|---|---|---|---|
| **Google Test(目标形态)** | `tests/gtest/*.cpp` | `TEST()/TEST_F()/TEST_P()`,单一 `olive-gtest` 二进制 | `tests/gtest/CMakeLists.txt`,共享 `main.cpp`(QApplication + offscreen + OCIO) |
| 自研 OAK 宏框架 | `tests/timeline/timeline-tests.cpp`、`tests/compositing/compositing-tests.cpp` | `OAK_ADD_TEST(name)` + `OAK_ASSERT(x)` | `tests/CMakeLists.txt` 的 `olive_add_test()` 宏,正则扫宏生成 `main()` |
| 纯 C assert | `engine/tests/oakengine_*_test.cpp`、`core/tests/oakcore_*_test.cpp` | 手写 `main()` + `assert()` | `engine/CMakeLists.txt` 的 `make_oakengine_test()`,每个文件一个独立 ctest 二进制 |
## 2. 为什么统一到 Google Test
1. **断言可读性**:`assert(x)` 失败只说行号;`EXPECT_EQ(a, b)` 打印左右值,
定位快一个数量级。这正是近几轮 facade 调试里最痛的一点。
2. **fixture 替代手工样板**:纯 C 测试里每个文件都手写 `oakengine_init` /
`setenv("XDG_*")` / 临时目录 / `oakengine_project_free`,gfixture 的
`SetUp()/TearDown()`/`SetUpTestSuite()` 一次性收口。
3. **`GTEST_SKIP()`**:GPU/缺资源用例优雅跳过(offscreen OpenGL 不可绘现在
靠崩/超时区分,不好维护)。
4. **过滤与重复**:`--gtest_filter`、重复运行(压 flaky)、死亡测试。
5. **`assert()` 在 NDEBUG 下被吞**:纯 C 测试一旦开 Release 编译就形同虚设,
这是统一的硬理由之一。
## 3. 目标结构
```
tests/gtest/ # app 集成测试(已是 gtest,保持不变,按需并入新用例)
engine/tests/ # liboakengine facade 测试,改写为 gtest
CMakeLists.txt # 一个 oakengine_gtest 目标 + gtest_discover_tests
core/tests/ # liboakcore 测试,改写为 gtest
CMakeLists.txt # 一个 oakcore_gtest 目标 + gtest_discover_tests
tests/timeline/ # 删除 olive_add_test 产物,timeline-tests.cpp 改写为 gtest
tests/compositing/ # 同上
```
- `tests/CMakeLists.txt` 的 `olive_add_test()` 宏与 `tests/testutil.h` 的
`OAK_ADD_TEST`/`OAK_ASSERT`/`OAK_TEST_END` 宏全部删除。
- `engine/CMakeLists.txt` 的 `make_oakengine_test()` 宏删除。
- 每个新 gtest 二进制经 `gtest_discover_tests(<target>)` 进 ctest;**ctest 总
用例数不得少于迁移前**(迁移前列一张基线清单核对)。
## 4. 转换配方
### 4.1 OAK_ADD_TEST 宏框架(tests/timeline、tests/compositing)
| 旧 | 新 |
|---|---|
| `OAK_ADD_TEST(name)` | `TEST(SuiteName, name)` |
| `OAK_ASSERT(x)` | `ASSERT_TRUE(x)` |
| `OAK_ASSERT_EQUAL(a, b)` | `ASSERT_EQ(a, b)`(自定义宏会打印左右值,直接换掉) |
| `TIMELINE_TEST_START`(ColorManager::set_up_default_config + Project + Sequence) | `class TimelineTest : public ::testing::Test { void SetUp() override {...} }` |
| `OAK_TEST_END / return OLIVE_TEST_SUCCESS` | 删除(gtest 自动判过) |
例:
```cpp
// 旧
OAK_ADD_TEST(add_track) {
TIMELINE_TEST_START;
OAK_ASSERT(sequence.track_list(Track::k_video)->get_track_count() == 1);
}
// 新
TEST_F(TimelineTest, AddTrack) {
ASSERT_EQ(sequence.track_list(Track::k_video)->get_track_count(), 1);
}
```
### 4.2 纯 C assert(engine/tests、core/tests)
| 旧 | 新 |
|---|---|
| 手写 `int main()` | 删除,链接共享 gtest main |
| `assert(x)` | `ASSERT_TRUE(x)` / `EXPECT_TRUE(x)` |
| `assert(fabs(a-b) < eps)` | `EXPECT_NEAR(a, b, eps)` |
| `assert(strcmp(a, b) == 0)` | `EXPECT_STREQ(a, b)` |
| `make_tmpdir()` + `setenv("XDG_*")` | `SetUpTestSuite()` 里建一次临时目录 |
| 每文件自带 `oakengine_init/shutdown` | 共享 fixture 做(见 §5.2) |
**纯 C ABI 测试照写 C 调用**:gtest 文件是 C++,直接 `#include "oakengine/xxx.h"`
调 `oakengine_*` 函数即可,不需要把被测 API 改成 C++。断言里出现
`OakEngineNode*` 等不透明句柄比较用 `EXPECT_EQ((void*)a, (void*)b)`。
**过渡期技巧(可选,不推荐长期使用)**:文件量太大时可先加一个
`#define assert(x) ASSERT_TRUE(x)` 的兼容头,把 `main()` 删掉挂进 gtest,
再逐文件把 `assert` 换成语义化 `EXPECT_*`。但**最终态不许留 `assert()`**。
### 4.3 已是 Google Test 的(tests/gtest)
不动。新增的 engine/core 用例如需 app 侧对象,可直接加进 `olive-gtest` 目标。
## 5. 落地步骤(按顺序,每步闭环:构建 + 全量 ctest 绿 + 提交)
### 5.1 基线
先跑 `ctest -N` 记录迁移前用例总数,存为 `docs/zh/gtest-migration-baseline.md`
(迁移后对比,总数只增不减)。
### 5.2 共享 fixture/main
- `core/tests/main.cpp`:`RUN_ALL_TESTS` + `SetUpTestSuite` 建 XDG 临时目录。
- `engine/tests/main.cpp`:同上,外加 `oakengine_init(OAKENGINE_INIT_HEADLESS)`,
`TearDownTestSuite` 调 `oakengine_shutdown()`;`OAK_TEST_SOURCE_DIR` 经
`target_compile_definitions` 传入(照 `make_oakengine_test` 现有做法)。
- XDG 沙箱**每个二进制一份**,不要每个测试一份(与现状一致,避免并发冲突)。
### 5.3 core/tests(最小、无 Qt,先练手)
逐文件改写 `oakcore_*_test.cpp` 为 gtest,删手写 main;建 `oakcore_gtest` 目标
(链 `oakcore` + `GTest::gtest` + `GTest::gtest_main`),`gtest_discover_tests`。
全量 ctest 绿后提交。
### 5.4 tests/timeline、tests/compositing(OAK 宏框架)
按 §4.1 改写;建独立 gtest 目标或并入合适目标;删除 `olive_add_test` 调用、
`tests/testutil.h` 宏与 `tests/CMakeLists.txt` 中的宏定义。全量 ctest 绿后提交。
### 5.5 engine/tests(facade 测试,量最大)
按 §4.2 改写 `oakengine_*_test.cpp`;建 `oakengine_gtest` 目标(链 `oakengine`
+ Qt + gtest),删 `make_oakengine_test`。全量 ctest 绿后提交。
### 5.6 收尾
- `ctest -N` 对比基线(只增不减);全量 `--output-on-failure` 绿。
- 全仓库 grep 确认无 `OAK_ADD_TEST`/`OAK_ASSERT`/`make_oakengine_test`/
`olive_add_test` 残留。
- 更新 `docs/zh/` 相关文档与本指引标注"已完成"。
## 6. 注意事项(别踩坑)
1. **GPU/渲染用例**:沿用 `GTEST_SKIP()` 判定(参
`tests/gtest/render_worker_footage_test.cpp` 的 backend 检查与
viewer_display_repro_test 的 offscreen 跳过模式),不许靠超时/崩溃区分。
2. **offscreen/OCIO**:需要 QApplication 的用例共享 `tests/gtest/main.cpp` 的
环境初始化(offscreen QPA + OCIO 配置);engine/core 的无头用例走
`oakengine_init(HEADLESS)`,不要重复造 QApplication。
3. **测试数据路径**:`OAK_TEST_SOURCE_DIR` 必须经 CMake 定义传入
(`tests/demo.mp4` 等),不要硬编码相对路径。
4. **线程/事件**:facade 事件类测试(`oakengine_events_test` 等)依赖
DirectConnection 同步语义,迁移时保持原用例的线程假设,不要引入
`QCoreApplication::processEvents` 之外的等待方式。
5. **一次性迁移 vs 渐进**:按 §5 的顺序渐进,**禁止**先删框架再慢慢补测试
(会造成不可测试的空窗)。每步都必须全量绿。
6. **ctest 仍是入口**:CI/本地都继续用 `ctest --output-on-failure -j$(nproc)`;
`gtest_discover_tests` 注册后,单个用例可用 `ctest -R <SuiteName.CaseName>`
或 `./<binary> --gtest_filter=...` 跑。
+319
View File
@@ -0,0 +1,319 @@
# RIIR 绞杀者模式执行计划:liboakengine 模块化拆分与渐进式 Rust 重写
> 本文档描述在 C ABI 迁移战役(见 `c-abi-migration-handoff.md`)完成之后,
> 如何用绞杀者模式(Strangler Fig)把 liboakengine.so 安全地拆成若干小模块,
> 再逐个重写为 Rust。
> **核心约束:每一步都可验证、可回退;任何一步失败都不影响已验证的部分。**
>
> 本文档面向未来的执行者(可能没有本文写作时的对话上下文),因此关键决策、
> 依据和验证方法都写成自包含的形式。与交接文档的关系:交接文档管"ABI 迁移战役"
> (消灭 oak-editor 对 olive:: 的引用、liboakengine 只导出 oakengine_*),
> 本文档管那之后的"拆分与重写战役"。**拆分的前置条件是 ABI 迁移完成(§1.1)。**
---
**API 冻结保证(最高优先级约束,先于一切拆分)**
**拆分模块,但公共 API 一个不动。** 这是整个战役的硬约束,凌驾于任何
"拆得更细"的冲动之上。三条钉死:
1. **公共 `oakengine_*` 全程冻结。** 在整个拆分与 Rust 重写期间,
`engine/include/oakengine/*.h` 里的每个公开函数:不改签名、不删函数、
不改语义。唯一允许的变更是**新增**函数,且必须标注 experimental。
新增不等于变更——既有签名与语义一个字都不许动。
2. **`liboakengine-facade` 自身不拆分。** facade 是唯一、薄、稳定的路由层,
公共 `oakengine_*` 全部保留在这里。拆分发生在它**之下**:facade 的内部
实现从"直接调 C++"改为"转发给对应小库的 C ABI",但**对外暴露的符号表
与调用约定完全不变**——app / oak-cli / render-worker / 未来 AI Agent
只链接 facade,连重新链接都不用。
3. **模块间内部 C ABI 与公共 API 分层、分别版本化。** 拆分后模块之间
(如 oakrender 调 oakmodel)不能再 C++ 直连,必须走**新增的内部 C ABI**。
这层接口:(a) 只对模块间可见,app 永远看不到;(b) 与公共 API 分开管理、
允许演进;(c) 命名与头文件路径必须明显区别于公共 facade(例如放
`engine/include/oakinternal/`,前缀 `oakinternal_`),杜绝"内部接口慢慢
变成事实公共 API"的漂移。公共 `oakengine_*` 另加**版本字段**
(`oakengine_api_version()`),让任何公共面的意外漂移可被检测。
> 一句话:**缝(公共 facade)冻死,缝后面的实现随便拆随便换。**
> 任何执行步骤如果会改动公共 `oakengine_*` 的既有签名或语义,就是走错了,
> 停下来回到本节。
---
## 0. 为什么这条路是可行的(三个已验证的事实)
1. **绞杀缝已经存在。** 迁移战役的最终产物就是一条稳定、纯 C、带测试覆盖的
ABI 缝(`engine/include/oakengine/*.h`,~30 个头、20+ 族)。绞杀者模式最危险
的一步——"在没有缝的系统里造缝"——已经由当前战役完成。
2. **插件模式在本仓库已跑通。** `ffmpeg_bridge`(独立 .so,C ABI)、
`oakgl`/`oakvulkan`(渲染后端插件,经 `engine/render/backend/renderbackend_c.h`
的 C ABI 由 `DynamicRenderer` 动态加载)证明"小 .so + C ABI + 运行时替换"
在本代码库不是理论,是现状。本计划只是把同一模式推广到全引擎。
3. **验证资产现成。** 44+ ctest、~2000 条 gtest、oak-cli(info/probe/render/
transcode,含 PPM 帧输出)、worker 端到端 harness(NDJSON 协议真实渲染)、
测试素材(demo.mp4/img.png/project_with_footage.ove)。每一步验证不需要
新建测试体系,只需要把它们固化为"门禁脚本"。
---
## 1. 目标、前置条件与非目标
### 1.1 前置条件(未满足前不动工)
- ABI 迁移战役完成:oak-editor / oak-render-worker `U _ZN5olive` = 0;
`nm -D --defined-only liboakengine.so | grep -c " T _Z"` = 0(visibility 收口);
全量 ctest 绿。(即交接文档 §1 的四条验收。)
- 本文 §4.2 的基础设施(Rust 工具链接入 + 门禁脚本)就位。
> **R6 收尾状态(2026-07-26 实测)**:
> - ✅ oak-editor / oak-render-worker `U _ZN5olive` = **0**(R5→R6 迁移战役完成,
> nm 58→0;交接文档 §6.4 豁免清单已清空为"无豁免");
> - ✅ 全量构建 0 error、全量 ctest 绿(45/45);app↔engine 边界对 app 的
> 引用而言已是纯 C ABI——**"边界已纯"**。
> - ⏳ **遗留项(不属 R6 范围,S0 需复核)**:engine 侧 visibility 收口未做——
> `nm -D --defined-only liboakengine.so | grep -c " T _Z"` 实测 = **3486**
> (导出 C++ 符号),尚未降到 0。该子条件是独立的收口工作(§2 Step 2 的
> visibility=hidden 规则),app 侧已无任何引用,收口不影响 app。
> - 已知遗留(已论证,不泄漏符号):app 仍 include 约 40 个 engine C++ 头
> (node/render/timeline/undo/pluginSupport,用于类型与 inline 访问器),
> nm=0 证明不产生符号引用;彻底清理超出 R6 的 58 符号目标,留待后续批次。
### 1.2 终态
- `liboakengine.so` 不复存在,取而代之的是一组小动态库(§3 模块图),
每个只有两种实现状态:C++(待重写)或 Rust(已重写)。
- app/worker/cli 只链接 **facade 壳库**(`liboakengine-facade`),对下层模块
的实现语言无感知。
- 任何模块的 Rust 替换都经过 §2 的六步流程,全程有 C++ 版本可回退,
直到 G6 退役门禁通过。
### 1.3 非目标(明确不做)
- 不重写 Qt、FFmpeg、OpenColorIO、PortAudio 等第三方库本身。
- 不重写 app(UI 层保持 C++/Qt;它消费的本来就是 C ABI)。
- 不改变 `oakengine_*` 公开 facade 的任何既有签名(拆分/重写只许改实现,
不许动契约;新增内部 ABI 允许,但必须符合同一套头文件规则)。
- 不做 big-bang:任何时刻整个系统都必须可构建、可测试、可发布。
---
## 2. 绞杀六步(每个模块的统一流程)
对每一个模块 X,严格按以下顺序执行;每步有对应门禁(§5),不过门禁不进下一步。
### Step 1 — 冻结 ABI
- 评审模块对外 C ABI 头(公开 facade 已有部分直接复用;模块间内部调用需要的
新增内部头,按 `c-abi-migration-handoff.md` §6.1 的同一套规则写:纯 C 类型、
buf/size 约定、owned/borrowed 注释、错误码)。
- 用门禁脚本生成 ABI 快照(§5-G1)并入库。**此后该头的任何改动都是显式评审行为。**
### Step 2 — 物理拆分(C++ 实现原样搬出)
- 新建 `liboakengine-<X>.so`:把该模块源码从 liboakengine 移入独立 CMake 目标;
原引擎内其他部分对它的 C++ 调用**全部改走它的 C ABI**。
- 新库同样 visibility=hidden + 只导出 C 符号。
- 过 G2:构建绿、全量 ctest 绿、符号审计绿、ABI diff = 0。
- **此步不改任何行为**——只搬代码和改调用方式。发现行为必须改才能拆的,
停下来记录,先回去补 facade(回 Step 1)。
### Step 3 — Rust 影子实现
- `rust/<X>/` 建 cdylib crate,实现与 Step 1 完全相同的 C ABI。
- cbindgen 生成的头与 C++ 头做规范化 diff(§5-G3),必须一致。
- FFI 边界硬规则:`catch_unwind` 全包裹(panic 不得跨 FFI)、错误码语义逐条
对齐、owned/borrowed 生命周期按注释实现(`Box::into_raw` / 借用引用)、
回调线程语义按契约复现(见 §6.2)。
### Step 4 — A/B 双跑
- CMake 选项 `OAK_MODULE_<X>_IMPL=cpp|rust` 控制链接哪个实现。
- 两种配置各自全量构建 + 全量 ctest + 金标准对比(§5-G4)。
- **帧级一致**:渲染输出字节一致或 PSNR ≥ 50dB;**序列化 round-trip 字节一致**;
其余以测试断言为准。
### Step 5 — 切换默认实现
- 默认实现切到 rust;CI 三平台构建 + 全量测试。
- C++ 实现保留一个发布周期作为回退选项(option 切回即可)。
### Step 6 — 退役
- 删除该模块的 C++ 实现与 cpp 构建分支;ABI 快照锁定为最终态;全量回归(G5 同项)。
- 在 roadmap 记录该模块重写完成。
---
## 3. 模块图与拆分顺序
### 3.1 依赖方向(单向,禁止循环;上层只经下层 C ABI 调用)
```
app / oak-cli / oak-render-worker
│
liboakengine-facade(壳:capi + 事件 + init)
│
┌────────┬────────┼─────────┬──────────┐
oaktask oakrender oakplugin oakaudio oakserialize
│ │ │ │ │
└────────┴────┬───┴──────────┴──────────┘
│
oakmodel(节点图 + 项目模型 + 时间线模型)
│
┌────────┼─────────┐
oakcodec liboakcore oakbackend(GPU 插件:oakgl/oakvulkan/未来的 Rust 后端)
│
ffmpeg_bridge(已是 C ABI .so)
```
**关键架构事实(拆分顺序的依据)**:
- `Node` 及其子类簇(Project/Folder/Footage/Sequence/Block/Track/Clip/Gap/
Transition/Subtitle/各效果节点)是 C++ 继承绑死的**不可拆分类型簇**——
跨模块做 C++ 继承不可能不导出 C++ 符号。因此它们必须整体作为一个模块
(oakmodel)处理,重写时也作为一个重写单元。这是本计划最大的一个拆分
粒度结论,不要再试图把 Block/Track 从 Node 里拆出去。
- `oakcore`(liboakcore.so)已是纯 C ABI 独立库,是天然的第一块 Rust 试验田
(见 M0)。
- GPU 后端已是插件,重写线与主线解耦(见 §7)。
### 3.2 执行顺序(依赖最少、Qt 最少、验证最容易的在前)
| 批次 | 模块 | 内容 | 前置依赖 | 主要风险 |
|---|---|---|---|---|
| M0 | **oakcore** | liboakcore 整体(rational/timecode/bezier/samplebuffer/audioparams,Qt-free) | 无 | 极低;工具链试金石 |
| M1 | **oakaudio** | AudioProcessor、AudioSynchronizer、AudioLevelMeter、波形计算 | oakcore | 低;顺带消掉 AudioProcessor 豁免项 |
| M2 | **oakcodec** | decoder/encoder/conform/proxy | ffmpeg_bridge | 中;FFmpeg 行为复刻 |
| M3 | **oakserialize** | node/project/serializer/*(XML 项目文件) | oakmodel(经 facade node/project 族) | 中;round-trip 必须字节一致 |
| M4 | **oakundo** | UndoCommand/UndoStack/MultiUndoCommand | oakmodel(经 facade) | 中;全局调用点多 |
| M5 | **oakrender** | RenderManager/ticket/watcher/cache/PreviewAutoCacher/ColorProcessor | oakmodel、oakcodec | 高;线程与 OCIO |
| M6 | **oakmodel** | Node/NodeInput/keyframe/traverser/factory/Project/Folder/Footage/Sequence/Block/Track/效果节点 | oakcore、oakcodec | 最高;最大类型簇 |
| M7 | **oaktask** | Task/TaskManager 及各任务类型 | oakmodel、oakrender | 中;QtConcurrent |
| M8 | **facade 壳 + 收尾** | capi 各实现、事件注册表、coreengine、config、plugin(OFX) | 全部 | 中;事件机制 Rust 化 |
每个批次内部都走 §2 的六步。**严格串行**:上一批次 G5 完成才开下一批次
(M0 例外,可与 ABI 迁移战役收尾并行准备)。
**为什么 oakmodel 排第六而不是第一**:它是依赖中心,先拆它会导致所有模块
都要先等它的内部 ABI 定型;先拆叶子模块可以用公开 facade(node.h/project.h/
timeline.h,本就是为外部消费设计的)充当模块间缝,缝的质量先被实战检验,
最后拆 oakmodel 时它的对外接口已经是稳定态。
---
## 4. 阶段计划
### 4.1 阶段 S0:前置确认(0 成本,只是检查)
- 对照 §1.1 逐条核对 ABI 迁移战役验收结果。未完成则停止,回到交接文档。
### 4.2 阶段 S1:重写基础设施(第一批真正的活)
1. **Rust 工具链接入**:仓库根建 `rust/` workspace;CMake 集成用 Corrosion
(cmake+cargo 标准方案);`cargo`/`cbindgen` 版本锁定并写进构建文档。
禁止要求全局安装 cargo 之外的 Rust 组件(CI 可复现)。
2. **门禁脚本固化**(全部进 CI):
- `scripts/abi-dump.sh`:对每个相关 .so 导出 `oakengine_*`/`oakcore_*` 符号 +
头文件规范化哈希,产出快照文件;`scripts/abi-check.sh` 与入库快照 diff。
- `scripts/golden-render.sh`:oak-cli render demo.mp4 指定帧 → PPM,
与金标准比对(字节一致或 PSNR ≥ 50dB);`oak-cli transcode` PPM 同理;
project_with_footage.ove 序列化 round-trip 字节一致;worker E2E harness。
- `scripts/symbol-audit.sh`:现有 nm 三件套的脚本化(app 侧 `U _ZN5olive`、
各 .so `T _Z`、facade 测试覆盖审计)。
3. **M0:liboakcore Rust 重写**(试点)。完整走六步,目的是把工具链、A/B 流程、
门禁全部打通并暴露问题。它是全仓库最小最干净的模块,失败成本最低。
M0 没全绿之前,不允许排产任何后续模块。
### 4.3 阶段 S2–S8:M1–M8
按 §3.2 表格逐模块执行。每模块的"模块档案"(边界清单、Qt 依赖清单、
信号清单、线程语义、验证重点)在 Step 1 时补写到本文 §6 对应小节。
---
## 5. 验证门禁(每步必须过,脚本化、进 CI)
| 门禁 | 触发步 | 内容 | 通过标准 |
|---|---|---|---|
| G0 | 每批开始 | 全量构建 + 全量 ctest + golden-render 基线快照 | 全绿,快照入库 |
| G1 | Step 1 | abi-dump 快照 | 与上一基线 diff 仅含本批新增 |
| G2 | Step 2 | 构建 + 全量 ctest + 新库符号审计 + ABI diff | 全绿;新库导出仅 C;diff=0 |
| G3 | Step 3 | cbindgen 头 vs C++ 头规范化 diff;crate 单测 | diff=0;单测全过 |
| G4 | Step 4 | 双实现配置各自全量测试 + golden 对比 | 两轮全绿;帧/序列化一致 |
| G5 | Step 5 | 三平台构建 + 全量测试 + 性能抽测 | 全绿;渲染帧耗时回退 ≤10% |
| G6 | Step 6 | 删除 C++ 实现后全量回归 + ABI 快照锁定 | 全绿;快照入库 |
- **性能抽测**:golden-render 脚本记录渲染耗时,Rust 版慢于 C++ 版 10% 以上
必须查明原因(允许记录后放行,但不允许无声劣化)。
- **回退规则**:任何门禁失败 → 停止该模块,切回 cpp 实现(Step 4 之后才有
可切对象;之前是天然回退态),记录原因,系统保持全绿。
---
## 6. 跨模块设计约束(全部钉死)
### 6.1 信号/通知的 Rust 化
- engine 的 QObject 信号是当前变更通知机制;capi/events.cpp 用 `dynamic_cast`
校验订阅 handle 的族类型。**Rust 对象没有 dynamic_cast**,因此在 M5(oakmodel
前置)之前必须引入**句柄类型标签约定**:所有 facade 句柄指向的对象首字段为
`uint32_t type_tag`(枚举值入 ABI 头),events.cpp 的族校验改为读标签。
这是对 events.cpp 的授权内改动,需配事件实测用例。
- Rust 模块的变更通知:经同一张 oakengine_event 注册表发射(事件机制是
app 侧唯一通道,Rust 模块只是换了发射端的实现语言)。
### 6.2 线程语义(ABI 契约,Rust 必须逐条复现)
- facade 回调/事件 = 发射线程同步调用(Qt::DirectConnection 等价),引擎对象
属 GUI 线程;回调内不得反调改同一对象的编辑原语。
- 渲染在后台线程(当前 QtConcurrent);Rust 侧线程模型自选(std::thread/
rayon/自建池),但**回调触发线程与顺序语义必须与原实现一致**;worker
NDJSON 协议的线程行为不得改变。
- 每模块 Step 1 时必须把该模块涉及的线程归属写进模块档案。
### 6.3 内存与生命周期
- owned/borrowed 规则按各 facade 头注释执行;Rust 侧 owned 句柄 `Box::into_raw`,
free 函数 `Box::from_raw` 回收;borrowed 句柄不接管析构。
- 禁止在 FFI 边界传递任何 Rust 特有类型(String/Vec/ trait object);边界上只有
C 类型,与现有头文件规则相同。
### 6.4 错误与 panic
- panic 不得跨 FFI(`catch_unwind` 全包裹,映射为 `OAKENGINE_E_FAILED` +
last_error 字符串)。错误码语义与 C++ 实现逐条一致(A/B 对比时断言)。
### 6.5 全局单例
- Config/NodeFactory/RenderManager/AudioManager 等单例,Rust 侧用 `OnceCell`/
显式注册表实现;初始化/销毁时机挂在 `oakengine_init`/`oakengine_shutdown`
既有钩子上,不引入新的隐式初始化。
### 6.6 第三方库
- FFmpeg:继续经 `ffmpeg_bridge`(已是 C ABI),Rust 模块链接桥库而非直接绑 FFmpeg。
- OCIO:ColorManager 是 C++ API 重度用户,M5 时评估:薄 C 封装进 facade vs
保留 C++ 微库长期共存(允许作为长期 C++ 孤岛,写入 roadmap)。
- Qt:只允许 facade 壳与 app 侧使用;M1 起的各引擎模块实现内**不得新增 Qt 依赖**
(QtCore 容器可暂用,但不得新增 QObject 信号、moc 类)。
---
## 7. GPU 后端平行线
- oakgl/oakvulkan 已是 `renderbackend_c.h` C ABI 插件,与主线解耦。
- Rust 后端(建议 wgpu 起步)作为**新插件**并行开发,通过同一 ABI 被
DynamicRenderer 加载;验收用现有 Backends gtest(Vulkan 用例即现成的
A/B 对比器——同一测试分别加载两个后端跑)。
- 不替代主线任何模块门禁;独立排期,不阻塞 M1–M8。
---
## 8. 风险登记册(开工前评审,施工中持续更新)
| 风险 | 等级 | 对策 |
|---|---|---|
| oakmodel 类型簇过大,六步周期过长 | 高 | Step 2 允许分子批拆分(先项目模型后效果节点),但 ABI 一次冻结 |
| 线程语义偏差导致偶发黑屏/崩溃 | 高 | G4 双跑必须包含 worker E2E 与 Backends viewer 用例;引入压力重复(每用例 ×10) |
| OCIO 无法 Rust 化 | 中 | 允许 C++ 孤岛(§6.6),不影响其他模块 |
| QtConcurrent 行为差异 | 中 | M5/M7 档案逐条记录并发模式;A/B 含并发压力 |
| cbindgen 头漂移 | 中 | G3 规范化 diff 进 CI,漂移即红 |
| 构建复杂度(cargo+cmake)拖慢迭代 | 中 | Corrosion 单一入口;文档固化;禁止手工 rustc |
| 行为不可拆(Step 2 发现必须改行为才能拆) | 中 | 回 Step 1 补 facade; roadmap 记录;禁止带行为变更进 Step 2 |
| 性能劣化 | 低 | G5 抽测阈值;剖析后放行或回退 |
---
## 9. 里程碑摘要(可直接抄进项目计划)
1. **S1 完成**:Rust 工具链 + 门禁脚本进 CI;M0(liboakcore)G6 退役。
2. **M1–M2 完成**:音频 DSP 与编解码 Rust 化;AudioProcessor 豁免项消除。
3. **M3–M4 完成**:序列化与 undo Rust 化;项目文件 round-trip 金标准常青。
4. **M5 完成**:渲染管线 Rust 化(OCIO 孤岛与否已裁决并记录)。
5. **M6 完成**:oakmodel Rust 化——**最大里程碑**,此后 liboakengine 主体为 Rust。
6. **M7–M8 完成**:任务系统与 facade 壳 Rust 化;liboakengine.so(C++ 版)正式退役。
7. **GPU 平行线**:Rust 后端插件经 Backends 双后端测试验收。
+213
View File
@@ -0,0 +1,213 @@
# Oak 主界面 UI 改版计划
> 本文是主界面重新设计的执行手册,面向 DeepSeek Flash(**不识字图,本文全部
> 用文字精确定义目标形态**)。详细程度对齐 `../r5-app-migration-guide.md`。
> 工作分支:`c-abi-migration`。**启动前提:R5(C ABI app 侧迁移)验收完成之
> 后**;启动后可与 Google Test 统一迁移并行——协调规则见 §2。
> 依据:`design/Oak-UI设计图-主界面-标注版.png`、`...-效果栈版.png`、
> `...-节点编辑器版.png`(共 10 项关键改动,本文逐项落地)。
---
## 1. 目标布局(文字定义,照此实现)
```
┌ 菜单栏:文件(F) 编辑(E) 视图(V) 回放(P) 序列(S) 窗口(W) 工具(T) 帮助(H)
├ 工具条(31px):14 个工具图标 + 吸附开关 + 缩放滑块 + 轨道高度滑块
├────────────────────────────────────────────────────────────────────
│ 素材查看器(源) │ 序列查看器(节目) │ 检查器│历史记录
│ ·适合/安全框 │ ·适合/安全框 │ ┌──────────────────┐
│ ·独立走带控制 │ ·节点编辑器(同组切换)│ 媒体 · xxx.mp4 │
│ ·1920×1080·25FPS │ ·分辨率·帧率信息 │ ▼ │
│ │ │ ≡ 变换 ✓ ×(卡片) │
│ │ 26px电平条(右缘) │ ≡ OCIO LUT ✓ ×(卡片)│
│ │ │ [+ 添加效果] │
│ │ │ └──────────────────┘
├────────────────────────────────────────────────────────────────────
│ 时间线(全宽贯通) │
│ 轨道头180px │ 轨道区 │
│ V2 视频轨道1 [锁定][显示] ████████ │
│ V1 视频轨道0 [锁定][显示] ████████████ │
│ A1 音频轨道0 [锁定][静音][独奏] ▁▂▃▅▂▁ │
│ A2 音频轨道1 [锁定][静音][独奏] ▁▂▁▃▂▁ │
├────────────────────────────────────────────────────────────────────
│ 状态栏:就绪 | 缓存:已启用 | 代理:关 | 自动保存:3分钟前 || 时间码/时长 | 25FPS | 1920×1080
└────────────────────────────────────────────────────────────────────
```
节点编辑器视图(与序列查看器同组切换、占中央最大面板):
```
┌ 节点编辑器(中央最大面板)
│ [+] [-] [适配] ┌ 检查器(同效果栈) ┐
│ ┌ 第一稿.mp4[视频] · 00:00:00:00–00:04:18:18 ┐ │ │
│ │ [媒体]→[变换]→[OCIO LUT]→[输出] │ │ │
│ └───────────────────────────────────────┘ │ │
│ ┌ 第一稿.mp4[音频] · 00:00:00:00–00:04:18:18 ┐ │ │
│ │ [媒体]→[音量]→[输出] │ │ │
│ └───────────────────────────────────────┘ │ │
│ ┌ 小地图 ┐ │ │
└────────────────────────────────────────────────────────────────────
```
**核心原则:效果栈(检查器)与节点图是同一份节点数据的两种视图**——检查器按
「媒体 → 变换 → OCIO LUT → 输出」自上而下线性排卡片;节点编辑器把同一份数据
画成图。默认用户像传统软件一样在检查器里线性工作,需要分支合成时切到节点
编辑器。
## 2. 执行前提与并行协调
### 2.1 执行前提:R5 验收完成后启动
**本计划在 R5(C ABI app 侧迁移)验收完成之前不启动。** 这不是保守,是
依赖关系:
- WP4(检查器·效果栈)落在 `app/widget/nodeparamview/`、WP2(时间线轨道
头)落在 `app/widget/timelinewidget/`、WP5(节点编辑器移位)落在
`app/widget/nodeview/`——这些都是 R5 符号消除的主战场。R5 先把这些文件
的 engine 调用点换到 facade(行为不变),本计划再在其上做 UI 重构,
面对的才是干净的 facade 边界;提前动手只会和 R5 互相踩踏。
- R5 完成后不存在文件重叠问题,**全部 WP 无需错峰**,按 §4 顺序执行即可。
启动时仍需遵守的红线(R5 的成果,永久有效):
- **禁止**:在本计划里改 `oakengine_*` 签名、新建 engine 命令类、或把
engine 源码再编进 app。
- 全部改动限 **app 侧 UI 代码**(`app/`),不加 engine 符号。
### 2.2 与 Google Test 统一迁移(`gtest-migration-guide.md`)的并行协调
**结论:可以完全并行,无错峰要求。** 两份计划的文件域不相交:
| 计划 | 动的文件 |
|---|---|
| UI 改版(本文) | `app/`(widget、panel、window、dialog、ts 翻译) |
| GTest 统一迁移 | `tests/`、`engine/tests/`、`core/tests/` 及三处测试 CMake |
唯一的接触点与规则:
- **`tests/gtest/`**:GTest 迁移明确"不动已有 gtest";本计划 §5 要求新增
UI 逻辑用例,新用例**直接加进 `tests/gtest/` 现有 `olive-gtest` 目标**,
不新建测试二进制。两边若同时改 `tests/gtest/CMakeLists.txt`,后提交者
普通三路合并即可(都是追加行,不会语义冲突)。
- **ctest 基线**:GTest 迁移的硬门槛是"用例总数只增不减";本计划只**新增**
用例、不删不改旧用例,天然满足。两边都以全量
`ctest --output-on-failure` 绿为提交前提,谁先跑谁后跑无所谓。
- **共享入口约定**:ctest 仍是唯一运行入口(gtest 仅编写框架),本计划
新增用例同样遵守,不引入别的测试框架或独立 runner。
- **已知 flaky**(`oak_cli_transcode`、`oakengine_export_test`、
`olive-gtest` 单次失败需单独重跑)是两个计划共同的背景噪音,判定规则
相同:单独重跑一次,连续两次失败才算回归。
## 3. 工作包(WP1–WP10,对应设计图 10 项)
### WP1 取消独立「工具」面板 → 31px 工具条
- **现状**:`app/panel/tool/tool.{h,cpp}` 是独立停靠面板,~8% 屏幕仅放 14 个图标。
- **目标**:删除该面板;在时间线(`app/widget/timelinewidget/`)上方加一条 31px
工具条,承载 14 个工具图标 + 吸附开关 + 缩放滑块 + 轨道高度滑块。
- **文件**:删/改 `app/panel/tool/`;`app/widget/timelinewidget/timelinewidget.{h,cpp}`
顶部加工具条;`app/panel/CMakeLists.txt`、`app/panel/panelmanager.cpp` 注册点。
- **验证**:工具条全部按钮功能与原面板一致;布局保存/恢复无该面板。
### WP2 时间线全宽贯通 + 轨道头 180px
- **现状**:时间线未全宽;轨道头窄,无统一的 显示/静音/独奏/锁定 与 V/A 编号命名。
- **目标**:时间线全宽;轨道头加宽至 180px 贴住轨道;视频轨「显示」、音频轨
「静音/独奏」、统一「锁定」;轨道以 `V2/V1/A1/A2` 编号 + 用途名(与主流 NLE 一致)。
- **文件**:`app/widget/timelinewidget/`(timelinewidget、trackview、trackviewitem、
timelineview)。**前提:R5 已完成 Track/ClipBlock facade 化,本 WP 在其上重构。**
- **验证**:轨道头控件改变 track 的 lock/mute/solo/show 状态;命名正确;全宽布局。
### WP3 双监看并列(源 + 节目)
- **现状**:`app/panel/footageviewer/`(源)与 `app/panel/sequenceviewer/`(节目)分开,
审素材对位需切换标签。
- **目标**:左右并排常显;素材查看器带独立走带控制(transport)。
- **文件**:`app/window/mainwindow/mainwindow.cpp` 布局;`app/panel/footageviewer/`、
`app/panel/sequenceviewer/`(KDDockWidgets 分组/dock 关系)。
- **验证**:两查看器同屏并列;源查看器独立走带;布局可保存/恢复。
### WP4 参数编辑器 → 「检查器·效果栈」
- **现状**:`app/widget/nodeparamview/` 是参数编辑器(item 列表 + 标题栏 + 关键帧控件)。
- **目标**:改为「检查器」面板(与「历史记录」同组标签)。自上而下线性排:
「媒体 · xxx.mp4」源卡 + 效果卡(变换、OCIO LUT、音量…)。**卡片规范**:
标题行 = ≡(拖拽排序) ▼(折叠) 名称 ✓(启停) ×(移除);底部「+ 添加效果」即搜即加。
每属性行右侧保留关键帧按钮(现 `nodeparamviewkeyframecontrol`)。
- **文件**:`app/widget/nodeparamview/`(nodeparamview、nodeparamviewitem、
nodeparamviewitemtitlebar、nodeparamviewdockarea、nodeparamviewwidgetbridge);
新增「检查器」容器/卡片模型。**前提:R5 已完成 Node 大族 facade 化,本 WP 在其上重构。**
- **验证**:卡片折叠/拖拽排序/启停/移除全部生效且写入节点图(undoable);
与节点编辑器视图数据一致;「+ 添加效果」可搜可加。
### WP5 节点编辑器移至中央最大面板 + 缩放/小地图
- **现状**:`app/widget/nodeview/` 节点编辑器非中央主区,大图易迷路。
- **目标**:移到中央最大面板(与序列查看器同组切换);新增缩放控件(+/−/适配)
与右下角小地图。
- **文件**:`app/widget/nodeview/`(nodeview、nodeviewcontext、nodeviewminimap)、
`app/panel/node/`、`app/window/mainwindow/mainwindow.cpp`。**前提:R5 已完成 Node 大族 facade 化,本 WP 在其上重构。**
- **验证**:节点编辑器占中央;缩放/适配/小地图可用;大图导航不迷路。
### WP6 音频监视器 → 26px 电平条
- **现状**:`app/widget/audiomonitor/`、`app/panel/audiomonitor/` 占一个完整面板。
- **目标**:改为 26px 超薄电平条,贴附在节目查看器右侧常显,释放一个面板。
- **文件**:`app/widget/audiomonitor/`、`app/panel/sequenceviewer/`(宿主)、
`app/panel/audiomonitor/`(去面板化)。
- **验证**:电平条 26px 常显、随播放电平跳动;原面板释放;布局可恢复。
### WP7 新增全局状态栏
- **现状**:`app/window/mainwindow/mainstatusbar.{h,cpp}` 仅显示 TaskManager 摘要。
- **目标**:全局状态栏显示:就绪状态、缓存、代理、自动保存时间(左);
当前时间码/时长、帧率、分辨率(右)。
- **文件**:`app/window/mainwindow/mainstatusbar.{h,cpp}`、`app/window/mainwindow/
mainwindow.cpp`。
- **验证**:各项信息实时刷新;与序列状态/缓存/代理/自动保存一致。
### WP8 查看器细节
- **现状**:填充条为蓝色;缩放/安全框入口不全;信息芯片不全;有与时间线重复的标尺。
- **目标**:填充条改黑;查看器右上角提供缩放与安全框按钮;信息芯片标注
分辨率与帧率;移除与时间线重复的标尺。
- **文件**:`app/panel/footageviewer/`、`app/panel/sequenceviewer/`、
`app/widget/viewer/`(viewer、viewerdisplay)。
- **验证**:外观与信息符合 §1 描述;无重复标尺。
### WP9 菜单访问键补全
- **现状**:`app/window/mainwindow/mainmenu.{h,cpp}` 的「窗口」无访问键 (W)。
- **目标**:「窗口」加 `(W)`,与系统及其他菜单项一致。
- **文件**:`app/window/mainwindow/mainmenu.{h,cpp}`。
- **验证**:菜单访问键完整一致。
### WP10 文案与格式统一
- **现状**:History 未汉化;项目面板日期为英文格式;首选项有英文残留。
- **目标**:History → 历史记录;项目面板日期改 `YYYY-MM-DD HH:mm`(如
`2026-06-03 20:25`);首选项英文残留(Behavior、Enable hover focus、
`1 minute(s)` 等)全部汉化。
- **文件**:`app/panel/history/`、项目面板(`app/widget/projectexplorer/`)、
首选项(`app/dialog/preferences/`)、`app/ts/*.ts` 翻译。
- **验证**:文案全部汉化、日期格式统一。
## 4. 执行顺序
R5 已验收完成(§2.1),无错峰约束。建议先做无依赖的布局项热身,再做
重构量大的三个 WP:
```
第一波(布局类,互相独立): WP1 → WP3 → WP6 → WP7 → WP8 → WP9 → WP10
第二波(重构类,建议在 R5 facade 化后的干净边界上做):
WP4(检查器)→ WP5(节点编辑器)→ WP2(时间线轨道头)
```
**每 WP 闭环**:现状 grep → 实现 → 全量构建 0 error → 全量 ctest 44/44 绿
(UI 改动不得引入回归)→ 立即提交 → roadmap 补记。
## 5. 测试要求(沿用项目规则)
- 所有测试用 **Google Test**(`CONTRIBUTING.md` 已立规)。
- 检查器卡片模型、效果栈↔节点图数据一致性、轨道头控件状态、状态栏信息、
工具条功能:补 Google Test 用例(`tests/gtest/`,`gtest_discover_tests`)。
- UI 行为改动以现有 `olive-gtest` 不回归为底线;新增可测逻辑(卡片模型、
视图模型)必须有单测。
- 需要显示的用例沿用 offscreen QPA;渲染相关用例沿用 `GTEST_SKIP` 模式。
## 6. 不做
- 不改 `oakengine_*` 公共 API(见 `riir.md` 的 API 冻结保证)。
- 不动 engine 内部实现、不动 R5 的 facade 工作。
- 不重写底层渲染/播放路径;本计划只改 UI 布局、容器与交互。
- 不做 AI 相关 UI(属 `ai-agent-design.md` 范围,另行)。