Files
oak-editor/docs/zh/plans/r8-p3-node-param-abi.md
T
Mike-Solar 66d761b4b7 R8: finish app/ pure C ABI migration (P3-P9) and make OTIO required
- app/ no longer includes engine C++ headers nor holds engine C++ types:
  engine access goes through the oakengine C ABI plus C++ wrappers
  (oakutil/oaknode.h, oakutil/oakvideo.h) and app-local mirror types
  (tooltypes, trackreferencehandle, timelinecommonapp, keyframetypes,
  subtitleapp, serializedlayoutinfoapp, nodevaluehandle, sliderdisplaytypeapp)
- engine: new C ABI functions for block/track/clip/transition navigation
  and predicates, links, caches, waveform/playback, disk folder,
  sequence_track_list, node_free, footage_is_valid, block_get_track,
  get_brush; loadotio/saveotio ported to the current engine API
- OTIO is now a required dependency: CI and CD build it on every
  platform, FindOpenTimelineIO fixed for OTIO 0.16/0.19 (the old deps
  include requirement silently disabled OTIO everywhere), runtime
  libraries are bundled into packages and copied next to macOS binaries
  (oak_copy_otio_runtime)
- fix ProjectViewModel drag&drop mime read/write size mismatch (segfault)
- unify color label naming (k_olive -> "Oak") in the app-side mirror
- docs: OTIO required, FFmpeg minimum corrected to 6.0 (en/zh)
- gtest suite: 1925 passed, 0 failed
2026-07-31 22:46:52 +08:00

550 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
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.
# R8-P3 执行手册:清除 app/ 对 node/node.h + node/param.h 的直接引用
> 本手册是**纯执行指令**。所有设计决策已经做出,不要重新设计、不要引入
> 本手册之外的改动。遇到与本手册矛盾的事实时停下来报告,不要自行变通。
>
> 目标:`grep -rn '#include "node/node.h"\|#include "node/param.h"' app/` 结果为 0。
> 原则:**engine/ 侧零改动**。所需的全部 C ABI 函数已存在于
> `engine/include/oakengine/node.h`(映射表见第 3 节),本阶段只在 app/ 内工作。
---
## 落地状态(2026-07-29 修订,取代 §2.1 的 AppNodeInput 方案)
本阶段最终按**双适配器**形态落地,与下文 §2.1 的原始决策不同:
- 消费侧不直接调 C ABI,统一经过 C++ wrapper 层
`shared/include/oakutil/oaknode.h`namespace `oak`):
`Node`/`Project`/`Footage`/`Input`/`Keyframe`/`KeyframeTrackRef`/`InputPair`
+ `NodeCategory` 枚举 + `NodeConnection`/`ContextNodeItem` 结构。
wrapper 只做转发;owned/borrowed 语义见文件头注释。
- `app/common/nodeinputhandle.h``AppNodeInput`/`AppNodeInputPair`/
`AppNodeKeyframeTrackReference`/`AppNodeCategory` 方案)**已废弃并删除**
全部消费点迁到 `oak::` 类型。
- app/ 中 node/param 相关裸 `oakengine_*` 调用已清零;保留的裸调用均属
其它子系统,以 `// WRAPPER-GAP:` 注释登记(见下表),归后续批次建立
对应 wrapper 时清理。
WRAPPER-GAP 登记(按归属批次分组):
| 归属 | 函数族 | 位置(示例) |
|---|---|---|
| undo 批次 | `oakengine_undo_*``oakengine_node_*_command` | nodeview.cpp、nodeparamviewcontext.cpp、mainwindowundo.cpp |
| group 批次 | `oakengine_group_*`passthrough/resolve/create | nodeview.cpp、nodeparamview.cpp、nodeparamviewitem.cpp |
| traverse 批次 | `oakengine_traverse_*``oakengine_node_set_value_hint` | nodevaluetree.cpp、nodetableview.cpp |
| clipboard 批次 | `oakengine_clipboard_*``oakengine_nodes_delete_many` | nodeview.cpp |
| keyframe 查询 | 最早/最晚/最近 keyframe、`get_split_*` 等 | nodeparamviewkeyframecontrol.cpp |
| 杂项 | `Node::getPluginInstance``Node::get_input_property``oakengine_node_array_insert_at/remove_at``Node::brush()``Node::has_gizmos`、效果标志常量 | nodeparamviewitem.cpp、nodeparamview.cpp、nodeviewitem.cpp |
除原手册列出的文件外,本次一并完成了前半程未迁完的文件:
`nodeparamviewitem.h/.cpp``nodeparamview.h/.cpp``nodeparamviewcontext.cpp`
`nodeparamviewwidgetbridge.cpp``nodevaluetree.h/.cpp``curvewidget.h/.cpp`
`curveview.cpp``timebasedwidget.cpp``nodeviewscene.cpp``nodewidget.h`
`panel/node/node.h``panel/param/param.*``toolbar.cpp``mainwindow.cpp`
`mainwindowundo.h/.cpp``timelinewidget/tool/import.cpp`(仅一处)、
`hashstreamapp.cpp``tests/gtest/widget_panels_model_test.cpp`
### 落地后已知遗留(2026-07-29 Wave3 收尾后)
- ~~`app/widget/viewer/viewerdisplay.h``app/widget/colorwheel/*.cpp` 仍直接
include `node/node.h`~~Wave2 已清)。
- ~~`app/widget/nodeparamview/nodeparamviewwidgetbridge.h` 公开构造签名仍用
engine `NodeInput`~~Wave3 已收敛到 `oak::Input`)。
- app/ 全树唯一保留的违规 include:`app/widget/timebased/timebasedwidget.h`
的 engine `node/output/viewer/viewer.h``QPointer<ViewerOutput>` 需要完整
QObject 类型,C ABI 无节点销毁事件,文件内有论证,待 facade 增加销毁通知后清理)。
- `tests/gtest/` 5 个文件白盒引用 engine C++ 头(engine 级测试,不在 P3 范围)。
- `nodeparamviewkeyframecontrol.cpp` 等仍经 `node/value.h` 传递使用 engine
C++ keyframe 查询方法(见 WRAPPER-GAP 表 keyframe 查询行)。
- `app/core.cpp``app/dialog/otioproperties/otiopropertiesdialog.cpp`
OTIO 适配已在 OTIO 必需化后本机验证编译通过(/opt/otio 0.19 与
otio-install 0.16 双版本)。
- 全量对象编译 + 全量链接构建均 0 errorcmake-build-debug,含 OTIO)。
---
## 0. 前置条件(先确认再动手)
1. 工作树中 P2 的改动(node/value.h + node/keyframe.h 清理)应先已提交。
P3 必须基于干净工作树开始,结束时单独成一个 commit。
2. 本机(macOS)验证环境说明,**不要被全量构建失败迷惑**:
- `liboakengine.dylib` 链接依赖 `18aed979a``-U,__ZN5olive13k_app_versionE`
修复,确认该 commit 已在历史中。
- 本机未安装 OpenTimelineIO`app/core.cpp`
`app/dialog/otioproperties/otiopropertiesdialog.cpp` **永远无法在本机编译**
与本次改动无关,验证时跳过这两个文件。
- 验证编译用对象级构建(绕过 dylib 依赖与 OTIO 文件):
```bash
cd cmake-build-debug
ninja -t targets all | grep -oE 'app/CMakeFiles/libolive-editor\.dir/[^:]+\.o' \
| sort -u | grep -vE 'dir/core\.cpp\.o|otiopropertiesdialog\.cpp\.o' > /tmp/app_objs.txt
xargs ninja -j"$(sysctl -n hw.ncpu)" < /tmp/app_objs.txt
# engine 侧:
ninja engine/CMakeFiles/oakengine-obj.dir/all 2>/dev/null || ninja $(ninja -t targets all | grep -oE 'engine/CMakeFiles/oakengine-obj\.dir/[^:]+\.o' | sort -u)
```
- 每完成下面一个步骤就跑一次上述 app 对象编译,保持可编译状态。
---
## 1. 现状清单(20 个文件,调查于 P2 完成后)
事实:`engine/node/node.h:40` 自己 include `node/param.h`,所以 include
node/node.h 等于同时引入两个头。
| 分组 | 文件 | 直接 include | 关键用法 |
|---|---|---|---|
| A 琐碎 | `app/widget/nodecombobox/nodecombobox.h` | node/node.h | **完全未使用任何符号**,直接删 |
| A 琐碎 | `app/dialog/preferences/tabs/preferencesappearancetab.cpp` | node/node.h | 仅用 `Node::k_category_count`:72 |
| A 琐碎 | `app/widget/menu/factorymenu.h` | node/node.h | `Node::CategoryID`/`k_category_unknown`:44)、`Node*` 返回值(:52 |
| B 指针替换 | `app/widget/nodeparamview/nodeparamviewitembase.h` | node/node.h | `get_title_bar_text_from_node(Node*)`:52)一处 |
| B 指针替换 | `app/widget/nodetableview/nodetableview.h` | node/node.h | `QVector<Node*>` 参数、`QMap<Node*, QTreeWidgetItem*>`:44 |
| B 指针替换 | `app/widget/nodevaluetree/nodevaluetree.h` | node/node.h | `set_node(const NodeInput&, Rational)`:35)一处 |
| C nodeview | `app/widget/nodeview/nodeviewedge.h` | node/node.h | `Node* output_`、`NodeInput input_` 值成员(:125/127 |
| C nodeview | `app/widget/nodeview/nodeviewitem.h` | node/node.h | `Node::Position`:60/63)、`get_input()` 按值返回 NodeInput:75 |
| C nodeview | `app/widget/nodeview/nodeviewcontext.h` | node/node.h | `Node*` 成员/参数、`NodeInput` const 引用参数 |
| D TrackRef | `app/widget/nodetreeview/nodetreeview.h` | node/node.h | `NodeKeyframeTrackReference` 值存储、QHash key、signal 签名(:69-73 |
| D TrackRef | `app/widget/keyframeview/keyframeviewinputconnection.h` | node/node.h + node/param.h | `NodeKeyframe*`、`Node::get_keyframe_tracks()`:55-61 |
| E paramview | `app/widget/nodeparamview/nodeparamview.h` | node/node.h | `QVector<Node*>`、`QHash<Node*,Node*>`、slot 签名带 NodeInput |
| E paramview | `app/widget/nodeparamview/nodeparamviewarraywidget.h/.cpp` | param.h / node.h | `Node* node_`、`node_->input_array_size()`cpp:53 |
| E paramview | `app/widget/nodeparamview/nodeparamviewconnectedlabel.h/.cpp` | param.h / node.h | `NodeInput input_` 值成员、NodeInput 成员调用 |
| E paramview | `app/widget/nodeparamview/nodeparamviewitem.h` | node/node.h | `QHash<NodeInput,…>`、`QHash<NodeInputPair,…>`、signal 签名 |
| E paramview | `app/widget/nodeparamview/nodeparamviewkeyframecontrol.h` | node/param.h | `NodeInput input_` 值成员、slot 签名 |
| F 特殊 | `app/common/hashstreamapp.cpp` | node/param.h | NodeInput/NodeInputPair/NodeKeyframeTrackReference 的 qHash |
| F 特殊 | `app/widget/viewer/viewerdisplay.h` | node/node.h | `Node*` 参数/成员、`NodeValueRow`P5 遗留见 4.8 |
`Node::Position`、`Node::get_keyframe_tracks` 的真实用法比预想少:
`Node::Position` 仅 nodeviewitem.h 两处且**纯 UI 聚合,不触 engine**
`NodeKeyframeTrackReference` 仅 nodetreeview.h + keyframeviewinputconnection.h。
---
## 2. 核心决策(已定,照做即可)
### 2.1 新建 `app/common/nodeinputhandle.h`(本阶段唯一新增文件)
这是 P2 的 `nodevaluehandle.h` 的姊妹文件:app 侧值类型镜像 + 薄封装。
**完整内容如下,直接照抄创建**(许可证头与 P2 文件保持一致):
```cpp
/***
Oak - Non-Linear Video Editor
Copyright (C) 2026 Oak Team
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
#ifndef OAK_NODEINPUTHANDLE_H
#define OAK_NODEINPUTHANDLE_H
#include <QHash>
#include <QString>
#include "oakengine/node.h"
namespace olive
{
/**
* @brief App-local mirror of engine's olive::NodeInput (node/param.h).
*
* Value type identifying one input (or array element) on a node. The C ABI
* identifies inputs by (node, input_id, element), so this struct interoperates
* with oakengine_node_input_*() directly. Semantics must stay identical to the
* engine type: default-constructed is {nullptr, QString(), -1}.
*/
struct AppNodeInput {
AppNodeInput() : node(nullptr), element(-1) {}
AppNodeInput(OakEngineNode *n, const QString &i, int e = -1)
: node(n), input(i), element(e)
{
}
bool operator==(const AppNodeInput &rhs) const
{
return node == rhs.node && input == rhs.input &&
element == rhs.element;
}
bool operator!=(const AppNodeInput &rhs) const { return !(*this == rhs); }
bool operator<(const AppNodeInput &rhs) const
{
if (node != rhs.node) return node < rhs.node;
if (input != rhs.input) return input < rhs.input;
return element < rhs.element;
}
bool is_valid() const { return node != nullptr; }
OakEngineNode *node;
QString input;
int element;
};
inline uint qHash(const AppNodeInput &i, uint seed = 0)
{
return ::qHash(i.node, seed) ^ ::qHash(i.input, seed) ^
::qHash(i.element, seed);
}
/**
* @brief App-local mirror of engine's olive::NodeInputPair.
*
* NOTE: engine's NodeInputPair is {Node *node; QString input;} — NO element.
* Keep the same shape; it is used as the array-widget key.
*/
struct AppNodeInputPair {
bool operator==(const AppNodeInputPair &rhs) const
{
return node == rhs.node && input == rhs.input;
}
OakEngineNode *node = nullptr;
QString input;
};
inline uint qHash(const AppNodeInputPair &p, uint seed = 0)
{
return ::qHash(p.node, seed) ^ ::qHash(p.input, seed);
}
/**
* @brief App-local mirror of engine's olive::NodeKeyframeTrackReference.
*/
struct AppNodeKeyframeTrackReference {
AppNodeKeyframeTrackReference() : track(-1) {}
AppNodeKeyframeTrackReference(const AppNodeInput &i, int t = 0)
: input(i), track(t)
{
}
bool operator==(const AppNodeKeyframeTrackReference &rhs) const
{
return input == rhs.input && track == rhs.track;
}
bool operator!=(const AppNodeKeyframeTrackReference &rhs) const
{
return !(*this == rhs);
}
AppNodeInput input;
int track;
};
inline uint qHash(const AppNodeKeyframeTrackReference &r, uint seed = 0)
{
return qHash(r.input, seed) ^ ::qHash(r.track, seed);
}
/**
* @brief App-local mirror of engine's Node::CategoryID ordinals
* (engine/node/node.h). Values must stay in sync with the engine enum;
* oakengine_node_category_name() takes these ordinals directly.
*/
enum AppNodeCategory {
k_app_category_unknown = -1,
k_app_category_output,
k_app_category_generator,
k_app_category_math,
k_app_category_keying,
k_app_category_filter,
k_app_category_color,
k_app_category_time,
k_app_category_timeline,
k_app_category_transition,
k_app_category_distort,
k_app_category_project,
k_app_category_open_fx,
k_app_category_count
};
/* Thin wrappers over the C ABI replacing the NodeInput member functions that
* app code actually called. */
inline bool app_input_is_connected(const AppNodeInput &i)
{
return oakengine_node_input_is_connected(
i.node, i.input.toUtf8().constData()) != 0;
}
inline OakEngineNode *app_input_get_connected_node(const AppNodeInput &i)
{
return oakengine_node_input_get_connected_node(
i.node, i.input.toUtf8().constData(), i.element);
}
} // namespace olive
Q_DECLARE_METATYPE(olive::AppNodeInput)
Q_DECLARE_METATYPE(olive::AppNodeKeyframeTrackReference)
#endif // OAK_NODEINPUTHANDLE_H
```
要点:
- `AppNodeInput` 字段公开(node/input/element),替换代码里 `input_.node()`
→ `input_.node`、`input_.input()` → `input_.input`、`input_.element()`
→ `input_.element`。
- `Q_DECLARE_METATYPE` 使这两个类型可以进 Qt signal/slot 签名和
`QVariant::fromValue`item data)。这与 `OakEngineNode*` 进 MOC 的既有
先例一致;**不要**再把它们改成非 slot 规避。
### 2.2 `Node::Position` → nodeviewitem 本地纯 UI 结构
`NodeViewItem::get_node_position_data()` 只是把 item 自身的
`pos()+is_expanded()` 打包(nodeviewitem.cpp:120-123),`set_node_position`
只是解包(:137-141),完全不触 engine。决策:在 `nodeviewitem.h` 内定义
局部结构,不进公共头:
```cpp
struct NodeViewItemPosition {
QPointF position;
bool expanded = false;
};
```
签名改为 `NodeViewItemPosition get_node_position_data() const` /
`void set_node_position(const NodeViewItemPosition &pos)`。调用点
nodeview 相关 .cpp)同名替换即可。
### 2.3 `Node*` → `OakEngineNode*`
所有成员变量、参数、返回值、容器 key 机械替换。两者是同一指针的不同
opaque 类型,边界处用 `reinterpret_cast`(既有先例:
nodeparamview.h:152、connectedlabel.cpp 等)。EngineEventBridge 的信号
全部已经是 `OakEngineNode*` + `(QString input, int element)`lambda 里
把旧的 `NodeInput(reinterpret_cast<Node*>(source), input, element)` 改为
`AppNodeInput(source, input, element)`。
### 2.4 `Node::CategoryID` → `AppNodeCategory`
`preferencesappearancetab.cpp:72` 的 `Node::k_category_count` →
`k_app_category_count``factorymenu.h:44` 的参数类型与默认值
`Node::k_category_unknown` → `AppNodeCategory` / `k_app_category_unknown`。
`oakengine_node_category_name(i, …)` 的调用已经用 int 序数,不用动。
### 2.5 `NodeKeyframe*` / `Node::get_keyframe_tracks` → C ABI 循环
keyframeviewinputconnection.h:55-61 目前内联调用
`input_.input().node()->get_keyframe_tracks(input_.input()).at(input_.track())`
返回 `const QVector<NodeKeyframe*>&`。决策:
- 成员类型改为 `AppNodeKeyframeTrackReference input_;`(含 track)。
- `get_keyframes()` 改为**按值返回** `QVector<OakEngineKeyframe*>`,实现
移到 .cpp,用 C ABI 拼装:
```cpp
QVector<OakEngineKeyframe *> KeyframeViewInputConnection::get_keyframes() const
{
QVector<OakEngineKeyframe *> keys;
const int n = oakengine_node_keyframe_count_on_track(
input_.input.node, input_.input.input.toUtf8().constData(),
input_.input.element, input_.track);
keys.reserve(n);
for (int i = 0; i < n; i++) {
keys.append(oakengine_node_keyframe_handle_on_track(
input_.input.node, input_.input.input.toUtf8().constData(),
input_.input.element, input_.track, i));
}
return keys;
}
```
- 完成后 grep `get_keyframes()` 的全部调用方(keyframeview.cpp 等),
把 `NodeKeyframe*` 改为 `OakEngineKeyframe*`,属性访问改走 P2 已建立的
keyframe C ABI`oakengine_node_keyframe_*` / `OakEngineKeyframe` 访问器)。
若调用方用到某个没有 C ABI 对应的 keyframe 成员函数,**停下来报告**,
不要自己在 engine 加函数。
### 2.6 hashstreamapp.cpp 瘦身
删除 :31-44 的三个 qHashNodeInput/NodeInputPair/
NodeKeyframeTrackReference)——App 类型的 qHash 已由新头内联提供。
保留 Track::Reference 的 qHash 与 QDataStream 运算符(来自
`node/output/track/track.h`,属 P6 范围),删除
`#include "node/param.h"`,保留 `#include "node/output/track/track.h"`。
### 2.7 nodetreeview 顺带修一个既有 bug
nodetreeview.h:96-97 `k_item_input_reference` 与 `k_item_node_pointer`
都定义为 `Qt::UserRole + 1`。改为 `Qt::UserRole + 1` / `Qt::UserRole + 2`。
item data 中存的引用类型随之改为
`QVariant::fromValue(AppNodeKeyframeTrackReference)`。
### 2.8 viewerdisplay.h 只做最小改动
该头还有 `node/gizmo/text.h`、`node/output/track/tracklist.h`、
`node/color/colormanager/colormanager.h` 三处直接 include,属 P5/P6 范围,
**本阶段不动**。本阶段只做:
- 删除 `#include "node/node.h"`
- `set_gizmos(Node*)`、`generate_gizmo_transform(Node*, Node*, …)`、
成员 `Node *gizmos_` 改为 `OakEngineNode*`
- `NodeValueRow`、`NodeGizmo*`、`TextGizmo*` 维持现状(它们经 gizmo/text.h
链传递引入,P5 处理),.cpp 中把 `OakEngineNode*` 传给 gizmo C++ API 的
边界处加 `reinterpret_cast<Node*>`。
---
## 3. C ABI 映射表(证明 engine 零改动)
| 旧调用 | 替换 |
|---|---|
| `input.is_connected()` | `app_input_is_connected()`(新头,包装 `oakengine_node_input_is_connected` |
| `input.get_connected_output()` | `app_input_get_connected_node()`(包装 `oakengine_node_input_get_connected_node` |
| `input.node()/.input()/.element()` | `AppNodeInput` 公开字段 |
| `node->input_array_size(id)` | `oakengine_node_input_array_size` |
| `node->name()` | `oakengine_node_get_name` |
| `Node::disconnect` 等 | 已迁过(`oakengine_node_disconnect_ex` 等),不在本阶段 |
| `Node::get_keyframe_tracks` | `oakengine_node_keyframe_count_on_track` + `oakengine_node_keyframe_handle_on_track`(见 2.5 |
| `Node::k_category_count` / `CategoryID` | `AppNodeCategory`2.4 |
| `Node::Position` | 本地 `NodeViewItemPosition`2.2 |
| `NodeInputPair` | `AppNodeInputPair`(注意:无 element 字段) |
| `NodeKeyframeTrackReference` | `AppNodeKeyframeTrackReference`(字段 input/track 公开) |
---
## 4. 执行步骤(按序,每步后可编译)
### 4.1 步骤 1:新建头 + hashstreamapp 瘦身
- 按 2.1 创建 `app/common/nodeinputhandle.h`。
- 按 2.6 改 `app/common/hashstreamapp.cpp`。
- 编译验证(此时还没有使用方,只验证新头自身可编译:随便一个已改文件
include 它即可,或等到步骤 2 一起验证)。
### 4.2 步骤 2A 组(琐碎)
- `nodecombobox.h`:删除 `#include "node/node.h"`。
- `preferencesappearancetab.cpp`include 换 `common/nodeinputhandle.h`
:72 `Node::k_category_count` → `k_app_category_count`。
- `factorymenu.h`include 换 `common/nodeinputhandle.h`:44 参数类型
`Node::CategoryID` → `AppNodeCategory`,默认值 → `k_app_category_unknown`
:52 返回值 `Node*` → `OakEngineNode*`。检查 factorymenu.cpp 及调用方
`create_node_from_menu_action` 的使用处)同步改类型。
### 4.3 步骤 3:B 组(纯指针/引用替换)
- `nodeparamviewitembase.h``Node*` → `OakEngineNode*`;检查对应
.cpp 实现内部(如用 `n->name()`/`n->GetLabel()` 改 C ABI
`oakengine_node_get_name`/`oakengine_node_get_label`)。
- `nodetableview.h``QVector<Node*>` → `QVector<OakEngineNode*>`
`QMap<Node*, QTreeWidgetItem*>` → `QMap<OakEngineNode*, …>`。
该头用到的 `Rational` 来自 core`olive/core/...`),确保 include
core 头而不是靠 node.h 传递。
- `nodevaluetree.h``set_node(const NodeInput&, const Rational&)` →
`set_node(const AppNodeInput&, const Rational&)`.cpp 内
`input.node()/input()/element()` 改字段访问 + C ABI。
### 4.4 步骤 4C 组(nodeview 簇)
顺序:nodeviewedge.h → nodeviewitem.h → nodeviewcontext.h → 各自 .cpp。
- `nodeviewedge.h``Node* output_` → `OakEngineNode*`
`NodeInput input_` → `AppNodeInput input_``output()`/`input()` 返回类型
同步。构造函数参数同步。
- `nodeviewitem.h`:按 2.2 加 `NodeViewItemPosition``Node*` →
`OakEngineNode*``NodeInput get_input()` → `AppNodeInput get_input()`
(返回 `AppNodeInput(node_, input_, element_)`);
`get_item_for_input(NodeInput)` → `AppNodeInput`。
- `nodeviewcontext.h``Node*` → `OakEngineNode*`(含 `QMap`、`QHash` key、
`context_subs_`);`const NodeInput &` 参数 → `const AppNodeInput &`。
- 各 .cpp:桥接 lambda 按 2.3 组装 `AppNodeInput`;其它 Node 成员调用改
C ABI(绝大多数之前已迁,只剩类型改名)。
### 4.5 步骤 5D 组(TrackRef
- `nodetreeview.h`include 换 `common/nodeinputhandle.h`(保留已有的
`oakengine/node.h`);`NodeKeyframeTrackReference` →
`AppNodeKeyframeTrackReference`signals、成员、QHash key 全部);
按 2.7 修 UserRole bug`Node*` → `OakEngineNode*`。
- `nodetreeview.cpp``ref.input()` → `ref.input``ref.track()` →
`ref.track`item data 读写改 `QVariant::fromValue(...)`/`value<...>()`。
- `keyframeviewinputconnection.h/.cpp`:按 2.5。注意 `get_reference()`
返回类型改为 `AppNodeKeyframeTrackReference`。
### 4.6 步骤 6E 组(nodeparamview 簇,最大)
顺序:connectedlabel → keyframecontrol → arraywidget → item → view。
- `nodeparamviewconnectedlabel.h/.cpp`
- 成员 `NodeInput input_` → `AppNodeInput input_`
`Node *connected_node_` → `OakEngineNode *connected_node_`
- slots `input_connected/input_disconnected(OakEngineNode*, const NodeInput&)`
第二个参数改 `const AppNodeInput &`
- cpp:85-89 `input_.is_connected()`/`get_connected_output()` →
`app_input_is_connected(input_)`/`app_input_get_connected_node(input_)`
- cpp:95-108 lambda 里 `NodeInput(reinterpret_cast<Node*>(source), …)` →
`AppNodeInput(source, …)`
- cpp:180-181 的 `oakengine_node_disconnect_ex` 调用已合规,只需把
`input_.node()`/`input_.input()`/`input_.element()` 改字段访问;
- cpp:199 `connected_node_->name()` → `oakengine_node_get_name`
- `value_tree_->set_node(input_, …)` 随 4.3 的 nodevaluetree 改后自然兼容。
- `nodeparamviewkeyframecontrol.h``NodeInput input_` → `AppNodeInput`
slot `keyframe_enable_changed(const NodeInput&, bool)` → `AppNodeInput`
`get_connected_input()` 返回类型同步。
- `nodeparamviewarraywidget.h/.cpp``Node* node_` → `OakEngineNode*`
cpp:53 `node_->input_array_size(input_)` →
`oakengine_node_input_array_size(node_, input_.toUtf8().constData())`
cpp:45 的 `reinterpret_cast<void*>(node_)` 订阅保持不变。
- `nodeparamviewitem.h``QHash<NodeInput, InputUI>` →
`QHash<AppNodeInput, InputUI>``NodeInputPair` → `AppNodeInputPair`
signal `input_checked_changed(const NodeInput&, bool)` 与
slot `edge_changed(OakEngineNode*, const NodeInput&)` 改 `AppNodeInput`
`get_element_y(NodeInput)` → `AppNodeInput``Node*` → `OakEngineNode*`。
对应 .cppnodeparamviewitem.cpp)内 NodeInput 构造/比较改 AppNodeInput。
- `nodeparamview.h``QVector<Node*>` ×4 → `QVector<OakEngineNode*>`
`QHash<Node*, Node*>`paste 映射、generate_existing_paste_map)→
`QHash<OakEngineNode*, OakEngineNode*>`
`QHash<Node*, QPair<int64_t,int64_t>> context_subs_` → key 换类型;
slots `input_check_box_changed / group_input_passthrough_added/removed`
的 `const NodeInput &` → `const AppNodeInput &`
`get_snap_ignore_keyframes()` 返回 `std::vector<NodeKeyframe*>*` →
`std::vector<OakEngineKeyframe*>*`NodeKeyframe 经 node.h 传递而来,
随 include 删除必须一并处理;调用点 grep `get_snap_ignore_keyframes`
逐一适配,keyframe 属性访问走 P2 的 C ABI)。
### 4.7 步骤 7F 组(viewerdisplay.h
按 2.8 做最小改动。
### 4.8 步骤 8:验证 + 提交
```bash
# 1. 直接 include 清零
grep -rn '#include "node/node.h"\|#include "node/param.h"' app/
# 期望:无输出
# 2. engine 符号泄漏检查(app 对象中不应出现 olive::Node/NodeInput 等未定义符号)
# 对象编译全部通过后即视为通过
# 3. app 对象编译(命令见第 0 节)
# 4. engine 未动:git diff --stat -- engine/ 应为空
# 5. 提交(单独一个 commit
git add app/ docs/
git commit -m "R8 phase 3: replace node/node.h + node/param.h in app/ with C ABI and app-local value types"
```
---
## 5. 明令禁止
- 不要改 engine/ 下任何文件(包括 oakengine/node.h)。
- 不要把 `NodeInput` 等 engine 类型换成 `using` 别名指回 engine 头。
- 不要在本阶段处理 P4-P9 的范围(viewerdisplay.h 的 gizmo/tracklist/
colormanager include、nodeviewcontext.cpp 的 block.h/track.h/project.h、
hashstreamapp.cpp 的 track.h)。
- 不要"顺手"重构无关代码;唯一允许的顺手修复是 2.7 的 UserRole bug。
- 发现映射表覆盖不到的 engine 成员调用时,停下来报告,不要自行在
engine 加 C ABI 函数。
## 6. 已知遗留(写进提交说明/计划文档状态)
- `app/widget/viewer/viewerdisplay.h/.cpp` 仍有 gizmo/text.h、tracklist.h、
colormanager.h 直接 includeP5/P6)。
- `app/widget/nodeview/nodeviewcontext.cpp` 有 block.h、track.h、project.h、
sequence.h 直接 includeP3/P4 后续批)。
- `app/common/hashstreamapp.cpp` 保留 track.hP6)。