Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions app/widget/timebased/timescaledobject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,12 @@ void TimeScaledObject::set_maximum_scale(const double &max)
{
max_scale_ = max;

// Keep min <= max so the clamp in set_scale() is never called with
// inverted limits (undefined behavior)
if (min_scale_ > max_scale_) {
min_scale_ = max_scale_;
}

if (get_scale() > max_scale_) {
set_scale(max_scale_);
}
Expand All @@ -125,6 +131,12 @@ void TimeScaledObject::set_minimum_scale(const double &min)
{
min_scale_ = min;

// Keep min <= max so the clamp in set_scale() is never called with
// inverted limits (undefined behavior)
if (max_scale_ < min_scale_) {
max_scale_ = min_scale_;
}

if (get_scale() < min_scale_) {
set_scale(min_scale_);
}
Expand Down
10 changes: 10 additions & 0 deletions app/widget/timelinewidget/timelineandtrackview.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,16 @@ TimelineAndTrackView::TimelineAndTrackView(Qt::Alignment vertical_alignment,
splitter_->setSizes({ 180, width() });
}

TimelineAndTrackView::~TimelineAndTrackView()
{
// When the child views are destroyed they reset their scene, which can
// push a valueChanged through these connections while this object is
// already half-destroyed (Qt aborts on a slot invoked past its class's
// destructor). Detach first so teardown emissions go nowhere.
disconnect(view_->verticalScrollBar(), nullptr, this, nullptr);
disconnect(track_view_->verticalScrollBar(), nullptr, this, nullptr);
}

QSplitter *TimelineAndTrackView::splitter() const
{
return splitter_;
Expand Down
2 changes: 2 additions & 0 deletions app/widget/timelinewidget/timelineandtrackview.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ class TimelineAndTrackView : public QWidget {

TrackView *track_view() const;

virtual ~TimelineAndTrackView() override;

private:
QSplitter *splitter_;

Expand Down
73 changes: 73 additions & 0 deletions docs/plans/issues-9-20-dependency-plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Issues 9–20 (#34–#45): Dependency Analysis & Two-Person Split

Scope: the twelve "Structure migrations" sub-issues of #26 (Eliminating
EventBridge), GitHub issues #34–#45. Each is independently shippable; this
document captures the real ordering constraints so two people can work in
parallel without stepping on each other.

## Hard dependencies

Only two kinds exist:

1. **The issue 7 signal (`Core::undo_index_changed`)** — required by issues
9, 11, 15, 16 (and used by 10/12/13/17 for undo refresh).
✅ Already landed in `a030f2da2` (issue 7 / #55). **No longer blocks
anything.**

2. **The issue 15 "project load finished" hook** (`Core` broadcasts after
TaskDialog load success). Explicitly reused by:
- **issue 13 (#38)** — "re-read uniformly after undo/**load**"
- **issue 17 (#42)** — "undo and **load** rebuild uniformly"
- **issue 19 (#44)** — "on load completion do a uniform model reset
(**reuse the issue 15 hook**)"

Everything else has **no ordering constraints**.

## Dependency graph

```
issue 7 (done) ──┬─> 9, 10, 11, 12, 16 (undo refresh signal, available now)
└─> 15 ──┬─> 13 (project-load hook)
├─> 17
└─> 19
14, 18, 20 — fully independent
```

## File-overlap (merge-conflict) risks

| Pair | Overlap | Severity |
|------|---------|----------|
| 15 / 16 | both in `app/widget/nodeview/` (+ `mainwindow.cpp` in 16) | low — different files |
| 17 / 18 | both in `app/widget/timelinewidget/` (17 heavily edits `timelinewidget.cpp`) | medium — keep in the same person's queue |
| 12 / 13 / 14 | all under `app/widget/nodeparamview/` | low — different files |
| 9 / 10 | marker/workarea pattern is identical; not the same files | none, but cheap to do together |

## Suggested split

**Person A — "signals already available" batch (start immediately):**

- issue 9 (#34) — seekablewidget marker/workarea
- issue 10 (#35) — resizabletimelinescrollbar marker/workarea (same pattern as 9)
- issue 11 (#36) — nodeviewitem label/color/message/array
- issue 12 (#37) — NodeParamViewItem / arraywidget / keyframecontrol
- issue 14 (#39) — NodeParamView group passthrough / context
- issue 20 (#45) — misc leftovers

**Person B — "project-load hook" chain (15 first, then its dependents):**

- issue 15 (#40) — nodeviewcontext structure core + **add the load hook**
- issue 16 (#41) — nodeview NODE_REMOVED_FROM_GRAPH (same area as 15)
- issue 13 (#38) — nodeparamviewwidgetbridge values (needs the hook)
- issue 17 (#42) — timelinewidget track/block structure (needs the hook; keeps 18's neighbor in one queue)
- issue 18 (#43) — trackviewitem index/muted
- issue 19 (#44) — projectviewmodel folder/label (needs the hook)

Rationale: B owns everything that consumes the issue 15 hook, so the hook's
API is designed and used by one person with no cross-team blocking. A's
batch only needs the already-landed issue 7 signal, so both can start today.
Workload is ~6 × 0.5 day each side.

Ground rules still apply per issue: `cmake --build cmake-build-debug -j8 &&
cd cmake-build-debug && ctest -j4` must be green (122/122) before shipping,
and remove the matching `bridge_->subscribe` / `oakengine_event_subscribe`
calls when done.
70 changes: 70 additions & 0 deletions docs/zh/plans/issues-9-20-dependency-plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Issues 9–20(#34–#45):依赖关系梳理与两人分工建议

范围:#26(Eliminating EventBridge)的 12 个 "Structure migrations"
子任务,对应 GitHub issue #34–#45。每个 issue 都可以独立交付,本文档
梳理真实的先后约束,让两个人可以并行推进而互不阻塞。

## 硬依赖

只有两类:

1. **issue 7 的信号(`Core::undo_index_changed`)** —— issue 9、11、
15、16 明确依赖它(10/12/13/17 的 undo 刷新也要用)。
✅ 已在 `a030f2da2` 落地(issue 7 / #55)。**不再构成阻塞。**

2. **issue 15 新增的 "project load finished" 钩子**(Core 在 TaskDialog
加载成功后广播)。明确复用它的有:
- **issue 13(#38)** —— "re-read uniformly after undo/**load**"
- **issue 17(#42)** —— "undo and **load** rebuild uniformly"
- **issue 19(#44)** —— "on load completion do a uniform model
reset(**复用 issue 15 的钩子**)"

其余 issue **没有任何先后约束**。

## 依赖图

```
issue 7(已完成) ──┬─> 9, 10, 11, 12, 16 (undo 刷新信号,现已可用)
└─> 15 ──┬─> 13 (项目加载钩子)
├─> 17
└─> 19
14、18、20 —— 完全独立
```

## 文件重叠(合并冲突)风险

| 组合 | 重叠区域 | 程度 |
|------|---------|------|
| 15 / 16 | 都在 `app/widget/nodeview/`(16 还涉及 `mainwindow.cpp`) | 低——不同文件 |
| 17 / 18 | 都在 `app/widget/timelinewidget/`(17 会大改 `timelinewidget.cpp`) | 中——建议排在同一个人的队列里 |
| 12 / 13 / 14 | 都在 `app/widget/nodeparamview/` 下 | 低——不同文件 |
| 9 / 10 | marker/workarea 的处理模式完全相同,但不是同一批文件 | 无冲突,但连着做成本低 |

## 建议分工

**A 同学 —— "信号已就绪" 批次(今天就能开始):**

- issue 9(#34)—— seekablewidget marker/workarea
- issue 10(#35)—— resizabletimelinescrollbar marker/workarea(与 9 同模式)
- issue 11(#36)—— nodeviewitem label/color/message/array
- issue 12(#37)—— NodeParamViewItem / arraywidget / keyframecontrol
- issue 14(#39)—— NodeParamView group passthrough / context
- issue 20(#45)—— 零散收尾

**B 同学 —— "项目加载钩子" 链(先做 15,再做它的下游):**

- issue 15(#40)—— nodeviewcontext 结构核心 + **新增加载钩子**
- issue 16(#41)—— nodeview NODE_REMOVED_FROM_GRAPH(与 15 同区域)
- issue 13(#38)—— nodeparamviewwidgetbridge 参数值(依赖钩子)
- issue 17(#42)—— timelinewidget track/block 结构(依赖钩子;同时把 18 的邻域保持在同一队列)
- issue 18(#43)—— trackviewitem index/muted
- issue 19(#44)—— projectviewmodel folder/label(依赖钩子)

理由:所有消费 issue 15 钩子的 issue 都归 B,钩子的 API 由同一个人
设计并使用,零跨人阻塞;A 的批次只依赖已经落地的 issue 7 信号,
两人今天都能开工。工作量两侧均约为 6 × 0.5 天。

每个 issue 的通用要求不变:提交前
`cmake --build cmake-build-debug -j8 && cd cmake-build-debug && ctest -j4`
必须全绿(122/122),完成后移除对应的
`bridge_->subscribe` / `oakengine_event_subscribe` 调用。
109 changes: 102 additions & 7 deletions docs/zh/plans/riir.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,11 +147,11 @@
liboakengine-facade(壳:capi + 事件 + init)
┌────────┬────────┼─────────┬──────────┐
oaktask oakrender oakplugin oakaudio oakserialize
│ │ │ │ │
└────────┴────┬───┴──────────┴──────────┘
┌────────┬────────┼─────────┬──────────┬──────────────
oaktask oakrender oakplugin oakaudio oakserialize oakstorage
│ │ │ │ │ (工程文件读写,
└────────┴────┬───┴──────────┴──────────┘ 独立模块,未来
替换为数据库)
oakmodel(节点图 + 项目模型 + 时间线模型)
┌────────┼─────────┐
Expand All @@ -160,6 +160,99 @@
ffmpeg_bridge(已是 C ABI .so)
```

**oakstorage 单列说明(本计划对原模块图的唯一结构性修改)**:
工程文件的读写(`node/project/serializer` 的落盘路径 + `task/project/`
load/save/loadotio/saveotio 的文件 IO)从 oakserialize / oaktask 中**单独拆出**为
oakstorage 模块。它对上只暴露**存储后端无关**的 C ABI(打开/保存/探测工程,
URI 寻址),当前唯一后端是 XML .ove 文件;**未来替换为数据库时只新增一个
后端实现,上层(oaktask/facade)零改动**。剪贴板序列化(copy/paste)不属于
存储,仍留在 oakserialize。详细接口设计见 `riir/04-interfaces.md` 与
`riir/M10-oakstorage.md`。

### 3.1.1 模块数据流图(Mermaid)

下图描述终态各模块之间的调用与数据流向。**实线 = 命令调用(上层→下层,
内部 C ABI `oak<mod>_*`,调用方知道影响);虚线 = 仅两种允许的反向通知:
facade→app 的 `oakengine_event` 通道,以及异步任务(oaktask 任务 /
渲染 ticket / GPU 帧完成)的完成回调。下层对上层没有事件订阅。**

```mermaid
flowchart TB
subgraph 消费侧["消费侧(不感知实现语言)"]
APP["oak-editor (app)"]
CLI["oak-cli"]
WRK["oak-render-worker"]
end

FACADE["liboakengine-facade<br/>壳:oakengine_* 公共 C ABI(冻结)<br/>+ 事件注册表 + init/shutdown"]

subgraph 引擎模块["引擎模块(facade 之下,只经内部 C ABI 互调)"]
TASK["oaktask<br/>任务编排(Task/TaskManager)"]
RENDER["oakrender<br/>渲染管线/缓存/色彩"]
PLUGIN["oakplugin<br/>OpenFX 宿主"]
AUDIO["oakaudio<br/>音频 DSP/输出/电平"]
SERIAL["oakserialize<br/>节点图 XML 序列化<br/>(剪贴板 copy/paste,不落盘)"]
STORAGE["oakstorage<br/>工程持久化(URI 打开/保存/探测)<br/>后端可插拔:ove-xml 文件|未来 oakdb 数据库"]
UNDO["oakundo<br/>UndoStack/UndoCommand"]
MODEL["oakmodel<br/>Node 类型簇 + Project/Sequence/Track/Block<br/>(最大不可拆分类型簇)"]
CODEC["oakcodec<br/>decoder/encoder/conform/proxy"]
CORE["liboakcore<br/>rational/timecode/bezier/samplebuffer"]
BACKEND["oakbackend<br/>GPU 插件:oakgl / oakvulkan / 未来 Rust(wgpu)"]
FFMPEG["ffmpeg_bridge<br/>FFmpeg 的 C ABI 桥(现成 .so)"]
end

APP -->|"oakengine_* 调用"| FACADE
CLI --> FACADE
WRK --> FACADE
FACADE -.->|"oakengine_event 变更通知<br/>(发射线程同步回调)"| APP

FACADE -->|"任务工厂/进度"| TASK
FACADE -->|"渲染请求/帧句柄"| RENDER
FACADE --> AUDIO
FACADE --> PLUGIN
FACADE -->|"工程打开/保存(URI)"| STORAGE
FACADE --> UNDO

TASK -->|"load/save 任务委托<br/>(Project 句柄 + XML 字节流)"| STORAGE
TASK -->|"import/conform/proxy 任务"| CODEC
TASK -->|"导出任务"| RENDER

RENDER -->|"遍历节点图求值"| MODEL
RENDER -->|"解码帧请求"| CODEC
RENDER -->|"上传纹理/绘制"| BACKEND
RENDER -->|"OCIO 色彩变换"| CORE

STORAGE -->|"反序列化建图 / 序列化取图(同步命令)"| MODEL
SERIAL --> MODEL

MODEL -->|"读取媒体参数"| CODEC
MODEL --> CORE
UNDO -->|"命令持有 Node/Project 句柄"| MODEL
AUDIO --> CORE
CODEC --> FFMPEG
CODEC --> CORE
BACKEND -.->|"帧完成(异步任务回调,ticket 返回通道)"| RENDER
```

**数据流要点**:

1. **命令流(自上而下)**:app 的每个动作 → facade `oakengine_*` → 对应
模块内部 C ABI。facade 是唯一入口,模块不允许被 app 直接链接。
2. **工程 IO 流**:`oaktask` 创建 load/save 任务 → 委托 `oakstorage`;
oakstorage 按 URI scheme 选后端(`file://*.ove` → ove-xml 后端,
未来 `oakdb://` → 数据库后端),序列化/反序列化时对 `oakmodel` 建图
取图。**替换为数据库只发生在 oakstorage 内部。**
3. **帧数据流**:`oakcodec` 经 `ffmpeg_bridge` 解码 → `oakrender` 遍历
`oakmodel` 节点图求值 → `oakbackend`(GPU 插件)上屏/导出;帧以不透明
句柄 + buf/size 约定跨边界,不传递 C++ 对象。
4. **通知流(仅两种反向通道,虚线)**:模块间没有事件订阅——上层调
下层只有命令,调用方知道影响;变更通知由命令发起层(通常是 facade)
经 `oakengine_event` 同步回调给 app,Rust 化后只是发射端换语言,
通道不变(§6.1)。另一例外是异步任务的完成回调(oaktask 任务、
渲染 ticket、GPU 帧完成)——回调即该异步命令的返回通道。
5. **undo 流**:所有可撤销编辑(无论来自 facade 还是模块内部)都包装成
`OakUndoCommand` 进入 `oakundo` 栈,命令体内只持 oakmodel 句柄。

**关键架构事实(拆分顺序的依据)**:
- `Node` 及其子类簇(Project/Folder/Footage/Sequence/Block/Track/Clip/Gap/
Transition/Subtitle/各效果节点)是 C++ 继承绑死的**不可拆分类型簇**——
Expand All @@ -177,7 +270,8 @@
| 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 必须字节一致 |
| M3a | **oakstorage** | node/project/serializer 落盘路径 + task/project/{load,save,loadotio,saveotio} 文件 IO(工程持久化,后端可插拔:当前 XML 文件,未来数据库) | oakmodel(经 facade node/project 族) | 中;round-trip 必须字节一致;后端接口一次冻结 |
| M3b | **oakserialize** | node/project/serializer 的剪贴板/节点图 XML 序列化(copy/paste,不落盘) | 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 | 最高;最大类型簇 |
Expand Down Expand Up @@ -312,7 +406,8 @@ timeline.h,本就是为外部消费设计的)充当模块间缝,缝的质

1. **S1 完成**:Rust 工具链 + 门禁脚本进 CI;M0(liboakcore)G6 退役。
2. **M1–M2 完成**:音频 DSP 与编解码 Rust 化;AudioProcessor 豁免项消除。
3. **M3–M4 完成**:序列化与 undo Rust 化;项目文件 round-trip 金标准常青。
3. **M3a–M4 完成**:工程存储(oakstorage,含后端可插拔接口冻结)、剪贴板
序列化与 undo Rust 化;项目文件 round-trip 金标准常青。
4. **M5 完成**:渲染管线 Rust 化(OCIO 孤岛与否已裁决并记录)。
5. **M6 完成**:oakmodel Rust 化——**最大里程碑**,此后 liboakengine 主体为 Rust。
6. **M7–M8 完成**:任务系统与 facade 壳 Rust 化;liboakengine.so(C++ 版)正式退役。
Expand Down
15 changes: 9 additions & 6 deletions docs/zh/plans/riir/00-overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@
> 并稳定后,才逐模块用 Rust 重写(届时模块的 C ABI 原样保留,Rust
> 实现替换 C++ 实现对调用方透明)。
>
> 阅读顺序:`00`(本文)→ `01-adapter-pattern.md`(适配器规范,
> 所有模块共用)→ `02-modules-and-order.md`(模块清单、依赖矩阵、
> 拆分顺序)→ `03-testing.md`(测试规范)→ `M1`…`M9`(逐模块执行
> 阅读顺序:`00`(本文)→ `01-adapter-pattern.md`(适配器规范 + §0
> 接口铁律,所有模块共用)→ `02-modules-and-order.md`(模块清单、依赖矩阵、
> 拆分顺序)→ `03-testing.md`(测试规范)→ `04-interfaces.md`(模块间
> 接口 provides/consumes 全表)→ `M1`…`M10`(逐模块执行
> 手册,**C API 已在各手册中冻结**)。

## 目标与判据
Expand All @@ -18,9 +19,11 @@
```
oakcore(已有,不动)
oakcommon ─ oakundo ─ oaknode ─ oaktimeline ─ oakcodec ─ oakrender ─ oaktask ─ oakplugin
oakaudio ───────────────────┤
│ │
└────────────── oakstorage(工程持久化, ┘
后端可插拔:文件→数据库)
oakaudio ─────────────┐
liboakengine(= facade + coreengine,纯装配层)
```

Expand Down
Loading
Loading