@linker-design-plus/timeline-track 2.0.4 → 2.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -262
- package/dist/components/track/Track.d.ts +33 -3
- package/dist/core/controllers/demoPreviewRebuildState.d.ts +5 -4
- package/dist/core/controllers/domPreviewBackend.d.ts +2 -1
- package/dist/core/controllers/index.d.ts +2 -0
- package/dist/core/controllers/previewBackend.d.ts +23 -1
- package/dist/core/controllers/timelineClipConfigController.d.ts +1 -1
- package/dist/core/controllers/timelinePreviewRuntimeController.d.ts +40 -0
- package/dist/core/controllers/timelinePreviewSession.d.ts +29 -10
- package/dist/core/controllers/timelinePreviewStateController.d.ts +53 -0
- package/dist/core/controllers/timelineSelectionController.d.ts +1 -1
- package/dist/core/facade/timelineManager.d.ts +38 -16
- package/dist/core/models/types.d.ts +1 -1
- package/dist/core/presentation/timelinePresentationAdapter.d.ts +1 -1
- package/dist/core/stores/selectionStore.d.ts +2 -0
- package/dist/core/stores/timelineStore.d.ts +5 -0
- package/dist/core/tracks/timelineTrackCollection.d.ts +4 -4
- package/dist/core/tracks/trackManager.d.ts +1 -0
- package/dist/index.cjs.js +4 -4
- package/dist/index.es.js +8412 -7378
- package/dist/utils/logging/PreviewLoadTraceStore.d.ts +37 -0
- package/dist/utils/logging/index.d.ts +1 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,29 +1,30 @@
|
|
|
1
1
|
# @linker-design-plus/timeline-track
|
|
2
2
|
|
|
3
|
-
基于 TypeScript 和
|
|
3
|
+
基于 TypeScript、Konva 和 Vue demo 的时间线编辑组件库,对外以 `TimelineManager` 为统一入口,提供多轨片段编辑、拖拽/拉伸/分割、撤销重做、选中态同步、封面渲染和预览挂载能力。
|
|
4
4
|
|
|
5
5
|
## 当前状态
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
- `TimelineManager`
|
|
9
|
-
-
|
|
10
|
-
-
|
|
7
|
+
- 主线重构已经完成,仓库已从“双轨实现 + 大量兼容层”收敛到单一轨道组件 `Track`
|
|
8
|
+
- `TimelineManager` 仍然是 façade,但内部已经拆分为 `stores`、`commands`、`controllers`、`tracks`、`presentation`
|
|
9
|
+
- 当前发布包实际内置的预览实现是 `DomPreviewBackend`
|
|
10
|
+
- `previewBackend` 配置仍然保留,但当前实现会统一解析到 `dom`,不要把 `canvas` / `auto` 视为已交付能力
|
|
11
|
+
- 仓库内保留了较完整的回归测试,适合继续做功能收敛和架构瘦身
|
|
11
12
|
|
|
12
|
-
##
|
|
13
|
+
## 审计结论
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
|
|
21
|
-
-
|
|
15
|
+
本轮已完成一次针对仓库现状的维护梳理,并同步修正了以下残留:
|
|
16
|
+
|
|
17
|
+
- 修复了 3 处测试残留,使 `pnpm test` 与 `pnpm exec tsc -p tsconfig.json --noEmit` 恢复通过
|
|
18
|
+
- 清理了 `pixi` 预览类型和 demo 入口中的过期选项
|
|
19
|
+
- 把 README 与重构路线文档改为“当前真实状态”,不再把未交付的 canvas 预览写成已完成能力
|
|
20
|
+
- 把 `build` 脚本改为直接调用本地 `tsc`,避免 `npm run build:types` 带来的环境告警
|
|
21
|
+
|
|
22
|
+
更细的维护记录见 [docs/maintenance-audit.md](./docs/maintenance-audit.md)。
|
|
22
23
|
|
|
23
24
|
## 安装
|
|
24
25
|
|
|
25
26
|
```bash
|
|
26
|
-
|
|
27
|
+
pnpm add @linker-design-plus/timeline-track konva
|
|
27
28
|
```
|
|
28
29
|
|
|
29
30
|
## 快速开始
|
|
@@ -36,29 +37,9 @@ const preview = document.getElementById('preview-container') as HTMLDivElement;
|
|
|
36
37
|
|
|
37
38
|
const timeline = new TimelineManager({
|
|
38
39
|
container,
|
|
39
|
-
debug: true,
|
|
40
40
|
zoom: 100,
|
|
41
41
|
currentTime: 0,
|
|
42
|
-
previewBackend: 'dom'
|
|
43
|
-
previewSourceResolver: (clip) => {
|
|
44
|
-
if (clip.src.endsWith('.mp4')) {
|
|
45
|
-
return {
|
|
46
|
-
url: clip.src,
|
|
47
|
-
mimeType: 'video/mp4',
|
|
48
|
-
kind: 'mp4'
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
if (clip.src.endsWith('.m3u8')) {
|
|
53
|
-
return {
|
|
54
|
-
url: clip.src,
|
|
55
|
-
mimeType: 'application/vnd.apple.mpegurl',
|
|
56
|
-
kind: 'hls-fmp4'
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
return null;
|
|
61
|
-
}
|
|
42
|
+
previewBackend: 'dom'
|
|
62
43
|
});
|
|
63
44
|
|
|
64
45
|
timeline.attachPreview(preview);
|
|
@@ -72,72 +53,46 @@ await timeline.addClip({
|
|
|
72
53
|
x: 0.5,
|
|
73
54
|
y: 0.5,
|
|
74
55
|
scale: 1
|
|
75
|
-
}
|
|
76
|
-
thumbnails: ['https://example.com/thumb-1.jpg']
|
|
56
|
+
}
|
|
77
57
|
});
|
|
78
58
|
|
|
79
59
|
timeline.on('history_change', (_event, data) => {
|
|
80
|
-
console.log(
|
|
60
|
+
console.log(data.canUndo, data.canRedo);
|
|
81
61
|
});
|
|
82
|
-
|
|
83
|
-
timeline.play();
|
|
84
62
|
```
|
|
85
63
|
|
|
86
|
-
##
|
|
87
|
-
|
|
88
|
-
`ThumbnailProvider` 使用 `getThumbnails(clip)`,返回 `string[]` 或 `Promise<string[]>`。
|
|
89
|
-
音频 clip 默认不会使用缩略图渲染,`thumbnailProvider` 主要作用于视频 clip。
|
|
64
|
+
## 核心能力
|
|
90
65
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
];
|
|
100
|
-
}
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
const timeline = new TimelineManager({
|
|
104
|
-
container,
|
|
105
|
-
thumbnailProvider
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
timeline.setThumbnailProvider(thumbnailProvider);
|
|
109
|
-
await timeline.refreshAllClipThumbnails();
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
## 对外主 API
|
|
66
|
+
- 多轨时间线编辑,支持视频轨和音频轨
|
|
67
|
+
- 片段拖拽、拉伸、分割、跨轨移动和重叠避让
|
|
68
|
+
- 单一选中态与批量拖拽
|
|
69
|
+
- 播放头、缩放、滚动和时间同步
|
|
70
|
+
- 撤销 / 重做 / 历史变更通知
|
|
71
|
+
- 缩略图提供器与异步封面加载
|
|
72
|
+
- 预览容器挂载、预览比例同步和当前激活片段解析
|
|
73
|
+
- 草稿导入导出、音视频分离/恢复等编辑工作流
|
|
113
74
|
|
|
114
|
-
|
|
75
|
+
## 主要 API
|
|
115
76
|
|
|
116
77
|
常用方法:
|
|
117
78
|
|
|
118
79
|
- `play()` / `pause()` / `togglePlay()`
|
|
119
80
|
- `setCurrentTime(time)` / `getCurrentTime()`
|
|
120
|
-
- `setZoom(zoom)` / `
|
|
81
|
+
- `setZoom(zoom)` / `fitZoom()`
|
|
121
82
|
- `setSpeed(speed)` / `getSpeed()`
|
|
122
83
|
- `addClip(config)` / `addClips(configs)`
|
|
123
|
-
- `updateClip(clipId, updates)` / `removeClip(clipId)`
|
|
84
|
+
- `updateClip(clipId, updates)` / `removeClip(clipId)`
|
|
124
85
|
- `selectClip(clipId)` / `clearSelection()` / `getSelectedClip()`
|
|
125
|
-
- `
|
|
126
|
-
- `splitCurrentClip()` / `removeClipGaps()` / `fitZoom()`
|
|
86
|
+
- `splitCurrentClip()` / `removeClipGaps()`
|
|
127
87
|
- `undo()` / `redo()` / `clearHistory()`
|
|
128
88
|
- `attachPreview(containerOrConfig)` / `detachPreview()`
|
|
129
|
-
- `
|
|
130
|
-
- `getCurrentActiveClips()` / `getActiveClipsAtTime(time)`
|
|
131
|
-
- `getPreviewAspectRatio()` / `setPreviewAspectRatio({ width, height })` / `resetPreviewAspectRatioToAuto()`
|
|
132
|
-
- `exportTimeline()`
|
|
89
|
+
- `exportTimeline()` / `loadDraft(data)`
|
|
133
90
|
|
|
134
91
|
常用事件:
|
|
135
92
|
|
|
136
93
|
- `time_change`
|
|
137
94
|
- `play_state_change`
|
|
138
|
-
- `speed_change`
|
|
139
95
|
- `zoom_change`
|
|
140
|
-
- `preview_aspect_ratio_change`
|
|
141
96
|
- `clip_added`
|
|
142
97
|
- `clip_removed`
|
|
143
98
|
- `clip_updated`
|
|
@@ -146,10 +101,9 @@ await timeline.refreshAllClipThumbnails();
|
|
|
146
101
|
- `history_change`
|
|
147
102
|
- `track_duration_change`
|
|
148
103
|
- `buffering_state_change`
|
|
149
|
-
- `can_play_change`
|
|
150
104
|
- `source_loading_change`
|
|
151
105
|
|
|
152
|
-
##
|
|
106
|
+
## 关键类型
|
|
153
107
|
|
|
154
108
|
```ts
|
|
155
109
|
interface ClipConfig {
|
|
@@ -171,217 +125,68 @@ interface ClipConfig {
|
|
|
171
125
|
y: number;
|
|
172
126
|
scale: number;
|
|
173
127
|
};
|
|
174
|
-
|
|
175
|
-
|
|
128
|
+
trackId?: string;
|
|
129
|
+
volume?: number;
|
|
176
130
|
}
|
|
177
131
|
```
|
|
178
132
|
|
|
179
|
-
|
|
180
|
-
视频 clip 可通过 `visualTransform` 控制预览中的归一化位置和等比缩放,默认值为 `{ x: 0.5, y: 0.5, scale: 1 }`。
|
|
181
|
-
视频 clip 分离原声后会写入 `separatedAudioClipId`,对应音频 clip 会写入 `separatedFromVideoClipId`。
|
|
182
|
-
导出时间线时会包含 `composition`、轨道 `order` / `isMuted`、clip `isMuted` 和视频 clip 的 `visualTransform`,便于后端按当前多轨预览语义做合成。
|
|
133
|
+
## 预览实现说明
|
|
183
134
|
|
|
184
|
-
|
|
135
|
+
- 当前代码路径实际只会创建 `DomPreviewBackend`
|
|
136
|
+
- `previewBackend` 字段被保留,目的是不给后续预览实现扩展制造额外 breaking change
|
|
137
|
+
- 如果你在业务接入层看到 `canvas` / `auto` 配置,请把它当作保留参数,而不是当前版本的有效能力
|
|
185
138
|
|
|
186
|
-
|
|
139
|
+
## 架构概览
|
|
187
140
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
const timeline = new TimelineManager({
|
|
199
|
-
container,
|
|
200
|
-
previewBackend: 'canvas',
|
|
201
|
-
previewSourceResolver: (clip) => {
|
|
202
|
-
if (!clip.src.endsWith('.mp4')) {
|
|
203
|
-
return clip.src.endsWith('.m3u8')
|
|
204
|
-
? {
|
|
205
|
-
url: clip.src,
|
|
206
|
-
mimeType: 'application/vnd.apple.mpegurl',
|
|
207
|
-
kind: 'hls-fmp4'
|
|
208
|
-
}
|
|
209
|
-
: null;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
return {
|
|
213
|
-
url: clip.src,
|
|
214
|
-
mimeType: 'video/mp4',
|
|
215
|
-
kind: 'mp4'
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
});
|
|
141
|
+
```mermaid
|
|
142
|
+
flowchart TB
|
|
143
|
+
API["Public API\nsrc/index.ts"] --> FACADE["TimelineManager\nsrc/core/facade/timelineManager.ts"]
|
|
144
|
+
FACADE --> STORES["Stores\nTimeline / Selection / Playback / Viewport"]
|
|
145
|
+
FACADE --> COMMANDS["Commands\nTimelineCommands"]
|
|
146
|
+
FACADE --> CONTROLLERS["Controllers\nworkflow / selection / duration / playback / events"]
|
|
147
|
+
FACADE --> TRACKS["Tracks\nTrackManager / Bridge / Collection"]
|
|
148
|
+
FACADE --> PRESENTATION["Presentation\nTimelinePresentationAdapter"]
|
|
149
|
+
FACADE --> PREVIEW["Preview\nDomPreviewBackend"]
|
|
150
|
+
FACADE --> COMPONENTS["Konva Views\nTimeline / Track / Clip / Playhead / Panels"]
|
|
219
151
|
```
|
|
220
152
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
- `canvas` 后端仅面向 Chrome / Edge 桌面端
|
|
224
|
-
- 视频预览源支持 MP4,以及 `EXT-X-MAP + m4s` 形式的 fMP4 HLS
|
|
225
|
-
- 暂不支持基于 TS 分片的 HLS,也不支持 DRM 流
|
|
226
|
-
- Firefox / Safari 会自动回退到 `dom` 后端
|
|
153
|
+
当前最值得关注的热点:
|
|
227
154
|
|
|
228
|
-
|
|
155
|
+
- `src/core/facade/timelineManager.ts` 仍然超过 4k 行,是后续最重要的瘦身目标
|
|
156
|
+
- `src/components/track/Track.tsx` 内部还保留一层 `legacy` 交互镜像,用于兼容旧拖拽状态
|
|
157
|
+
- `src/utils/rendering/KonvaUtils.ts` 仍偏大,适合继续拆分为更小的渲染工具模块
|
|
158
|
+
- `demo/App.vue` 既承担演示又承担诊断面板职责,维护成本偏高
|
|
229
159
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
- [src/index.ts](./src/index.ts)
|
|
233
|
-
- [src/core/facade/timelineManager.ts](./src/core/facade/timelineManager.ts)
|
|
234
|
-
|
|
235
|
-
### 组件层
|
|
236
|
-
|
|
237
|
-
- [src/components/timeline/Timeline.tsx](./src/components/timeline/Timeline.tsx)
|
|
238
|
-
- [src/components/track/Track.tsx](./src/components/track/Track.tsx)
|
|
239
|
-
- [src/components/track/Clip.tsx](./src/components/track/Clip.tsx)
|
|
240
|
-
- [src/components/timeline/Playhead.tsx](./src/components/timeline/Playhead.tsx)
|
|
241
|
-
- [src/components/panel/TrackInfoPanel.tsx](./src/components/panel/TrackInfoPanel.tsx)
|
|
242
|
-
|
|
243
|
-
### 核心状态与命令
|
|
244
|
-
|
|
245
|
-
- `timelineStore` / `selectionStore` / `playbackStore` / `viewportStore`
|
|
246
|
-
- `timelineCommands`
|
|
247
|
-
- `timelineHistoryRecorder` / `timelineHistoryExecutor`
|
|
248
|
-
|
|
249
|
-
### façade 已拆出的控制器与适配层
|
|
250
|
-
|
|
251
|
-
- `timelineEventDispatcher`
|
|
252
|
-
- `timelineTrackInfoPanelController`
|
|
253
|
-
- `timelinePlaybackResolver`
|
|
254
|
-
- `timelinePreviewSession`
|
|
255
|
-
- `timelineSelectionController`
|
|
256
|
-
- `timelineDurationController`
|
|
257
|
-
- `timelineClipEventController`
|
|
258
|
-
- `timelinePresentationAdapter`
|
|
259
|
-
- `timelineTrackBridge`
|
|
260
|
-
- `timelineTrackCollection`
|
|
261
|
-
- `timelineTrackLayout`
|
|
262
|
-
|
|
263
|
-
### 工具层
|
|
264
|
-
|
|
265
|
-
- `clipVisualRenderer`
|
|
266
|
-
- `clipCoverRenderer`
|
|
267
|
-
- `timelineGridDrawing`
|
|
268
|
-
- `timeUtils`
|
|
269
|
-
- `KonvaUtils`
|
|
270
|
-
|
|
271
|
-
### 目录结构
|
|
160
|
+
## 目录结构
|
|
272
161
|
|
|
273
162
|
```text
|
|
274
163
|
src/
|
|
275
|
-
components/ Konva
|
|
276
|
-
core/
|
|
277
|
-
utils/
|
|
164
|
+
components/ Konva 组件与交互 helper
|
|
165
|
+
core/ stores / commands / controllers / facade / models
|
|
166
|
+
utils/ 渲染、时间和日志工具
|
|
278
167
|
styles/ 样式入口
|
|
279
168
|
index.ts 对外导出
|
|
280
169
|
docs/
|
|
281
170
|
interaction-model.md
|
|
171
|
+
maintenance-audit.md
|
|
282
172
|
refactor-roadmap.md
|
|
283
173
|
demo/
|
|
284
174
|
App.vue
|
|
285
175
|
main.ts
|
|
286
176
|
```
|
|
287
177
|
|
|
288
|
-
## 架构图级别说明
|
|
289
|
-
|
|
290
|
-
### 分层关系图
|
|
291
|
-
|
|
292
|
-
```mermaid
|
|
293
|
-
flowchart TB
|
|
294
|
-
API["Public API\nsrc/index.ts"] --> FACADE["TimelineManager façade\nsrc/core/facade/timelineManager.ts"]
|
|
295
|
-
|
|
296
|
-
FACADE --> STORES["Stores\nTimelineStore / PlaybackStore / SelectionStore / ViewportStore"]
|
|
297
|
-
FACADE --> COMMANDS["Commands\nTimelineCommands"]
|
|
298
|
-
FACADE --> CONTROLLERS["Controllers\nclip workflow / track mutation / playback / selection / duration / event dispatcher"]
|
|
299
|
-
FACADE --> HISTORY["History\nHistoryManager / Recorder / Executor"]
|
|
300
|
-
FACADE --> TRACKS["Tracks\nTrackManager / TimelineTrackBridge / TimelineTrackCollection"]
|
|
301
|
-
FACADE --> PRESENTATION["Presentation\nTimelinePresentationAdapter"]
|
|
302
|
-
FACADE --> PREVIEW["Preview Backend\nDomPreviewBackend / CanvasPreviewBackend"]
|
|
303
|
-
FACADE --> COMPONENTS["Konva Views\nTimeline / Track / Clip / Playhead / TrackInfoPanel"]
|
|
304
|
-
|
|
305
|
-
COMPONENTS --> INTERACTION["Interaction\nglobalPointerDragSession / trackInteractionState"]
|
|
306
|
-
COMPONENTS --> UTILS["Utils\ntime / rendering / logging"]
|
|
307
|
-
|
|
308
|
-
TRACKS --> COMPONENTS
|
|
309
|
-
COMMANDS --> STORES
|
|
310
|
-
CONTROLLERS --> STORES
|
|
311
|
-
CONTROLLERS --> HISTORY
|
|
312
|
-
CONTROLLERS --> TRACKS
|
|
313
|
-
PRESENTATION --> COMPONENTS
|
|
314
|
-
PREVIEW --> STORES
|
|
315
|
-
```
|
|
316
|
-
|
|
317
|
-
### 运行时职责图
|
|
318
|
-
|
|
319
|
-
```mermaid
|
|
320
|
-
flowchart LR
|
|
321
|
-
USER["用户操作\n拖拽 / 拉伸 / 分割 / 播放"] --> VIEW["组件层\nTrack / Clip / Timeline"]
|
|
322
|
-
VIEW --> BRIDGE["TrackBridge / 回调桥接"]
|
|
323
|
-
BRIDGE --> WORKFLOW["Controllers\n工作流编排"]
|
|
324
|
-
WORKFLOW --> COMMANDS2["Commands\n领域规则与变更计划"]
|
|
325
|
-
COMMANDS2 --> STATE["Stores + TrackCollection\n时间轴状态与轨道集合"]
|
|
326
|
-
WORKFLOW --> HISTORY2["History\n记录 undo / redo"]
|
|
327
|
-
WORKFLOW --> PREVIEW2["Preview Session\n同步多轨预览"]
|
|
328
|
-
STATE --> PRESENTATION2["PresentationAdapter\n把状态转换为渲染输入"]
|
|
329
|
-
PRESENTATION2 --> VIEW
|
|
330
|
-
WORKFLOW --> EVENTS["EventDispatcher\n对外事件通知"]
|
|
331
|
-
```
|
|
332
|
-
|
|
333
|
-
### 核心分层说明
|
|
334
|
-
|
|
335
|
-
- `src/index.ts`:对外导出层,暴露 `TimelineManager`、组件、`HistoryManager`、`TrackManager` 与模型类型。
|
|
336
|
-
- `src/core/facade/timelineManager.ts`:系统总入口,负责装配 store、commands、controllers、history、preview backend 与 Konva 视图。
|
|
337
|
-
- `src/core/stores/`:保存当前时间、播放状态、选中态、视口等最小状态源。
|
|
338
|
-
- `src/core/commands/`:封装纯领域规则,例如选中、查找 clip、跨轨移动准备、轨道放置规划。
|
|
339
|
-
- `src/core/controllers/`:把用户行为编排成完整工作流,处理副作用、事件派发、预览同步与轨道/clip 变更。
|
|
340
|
-
- `src/core/tracks/`:管理轨道集合、轨道桥接和轨道级别操作,是 façade 与 `Track` 组件之间的中间层。
|
|
341
|
-
- `src/core/presentation/`:把核心状态适配为视图输入,降低 façade 与 Konva 组件之间的耦合。
|
|
342
|
-
- `src/components/`:Konva 视图层,负责时间轴、轨道、clip、滚动条、播放头和面板渲染。
|
|
343
|
-
- `src/components/interaction/`:处理全局拖拽会话与交互状态机,保证拖拽移动/结束在窗口级别可靠收敛。
|
|
344
|
-
- `src/core/history/`:记录命令结果,提供撤销/重做闭环。
|
|
345
|
-
- `src/core/controllers/*Preview*`:抽象预览后端,支持 `dom` 与 `canvas` 两套多轨预览实现。
|
|
346
|
-
|
|
347
|
-
### 典型调用链路
|
|
348
|
-
|
|
349
|
-
#### 1. Clip 拖拽/跨轨移动
|
|
350
|
-
|
|
351
|
-
1. 用户在 `Track` / `Clip` 上产生指针操作。
|
|
352
|
-
2. `trackInteractionState` 识别当前操作类型,`globalPointerDragSession` 保证拖拽期间的全局 move/end 监听。
|
|
353
|
-
3. `Track` 通过回调把变更意图交给 `TimelineTrackBridge` 与 façade。
|
|
354
|
-
4. façade 调用 `timelineTrackMutationController`、`timelineClipWorkflowController`、`TimelineCommands` 计算目标轨道、位置与合法性。
|
|
355
|
-
5. 轨道集合与 store 更新后,同步写入 history,并触发重绘、事件通知与预览刷新。
|
|
356
|
-
|
|
357
|
-
#### 2. 播放与时间推进
|
|
358
|
-
|
|
359
|
-
1. `TimelineManager` 持有播放状态与当前时间。
|
|
360
|
-
2. `TimelinePlaybackResolver` 与预览 session 根据当前时间解析 active clips。
|
|
361
|
-
3. `DomPreviewBackend` 或 `CanvasPreviewBackend` 执行多轨音视频预览。
|
|
362
|
-
4. 时间变化通过 `TimelineEventDispatcher` 向外发布 `time_change`、`play_state_change` 等事件。
|
|
363
|
-
|
|
364
|
-
### 当前架构特点
|
|
365
|
-
|
|
366
|
-
- 对外仍保持单一 façade:调用方只需面向 `TimelineManager`。
|
|
367
|
-
- 内部已经拆成 store / commands / controllers / tracks / presentation,职责边界比早期单体 manager 更清晰。
|
|
368
|
-
- UI 交互和领域规则分离:交互细节主要留在 `components`,领域决策集中在 `core`。
|
|
369
|
-
- 预览后端可替换:预览能力通过 backend 抽象扩展,而不是直接耦合到时间轴视图。
|
|
370
|
-
- 历史系统贯穿工作流:新增、删除、更新、跨轨移动都能进入 undo / redo 闭环。
|
|
371
|
-
|
|
372
178
|
## 开发
|
|
373
179
|
|
|
374
180
|
```bash
|
|
375
181
|
pnpm install
|
|
376
|
-
pnpm
|
|
377
|
-
pnpm
|
|
182
|
+
pnpm dev
|
|
183
|
+
pnpm build
|
|
378
184
|
pnpm test
|
|
379
185
|
pnpm exec tsc -p tsconfig.json --noEmit
|
|
380
186
|
```
|
|
381
187
|
|
|
382
|
-
##
|
|
188
|
+
## 相关文档
|
|
383
189
|
|
|
384
|
-
-
|
|
385
|
-
-
|
|
386
|
-
-
|
|
387
|
-
- Safari(`dom` 后端)
|
|
190
|
+
- [docs/interaction-model.md](./docs/interaction-model.md): 指针交互分层和拖拽约束
|
|
191
|
+
- [docs/refactor-roadmap.md](./docs/refactor-roadmap.md): 主线重构归档与后续收敛方向
|
|
192
|
+
- [docs/maintenance-audit.md](./docs/maintenance-audit.md): 本轮仓库审计、验证结果和依赖建议
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import Konva from 'konva';
|
|
2
2
|
import { Clip as ClipType, ClipStateUpdate, TrackConfig, TimeMs, Theme, TrackType } from '../../core/models';
|
|
3
|
+
import { type TrackPointerOperation } from './trackInteractionState';
|
|
3
4
|
import type { MultiDragMoveRequest } from '../../core/tracks/timelineTrackBridge';
|
|
4
5
|
export declare class Track {
|
|
5
6
|
private static readonly DEFAULT_DRAG_ACTIVATION_THRESHOLD;
|
|
@@ -26,6 +27,7 @@ export declare class Track {
|
|
|
26
27
|
private multiDragOriginalPositions;
|
|
27
28
|
private promotedClipParents;
|
|
28
29
|
private interactionState;
|
|
30
|
+
private legacyInteractionSnapshot?;
|
|
29
31
|
private isVisualUpdate;
|
|
30
32
|
private onClipUpdate;
|
|
31
33
|
private onClipAdd;
|
|
@@ -55,8 +57,37 @@ export declare class Track {
|
|
|
55
57
|
private handleGlobalPointerMove;
|
|
56
58
|
private handleGlobalPointerEnd;
|
|
57
59
|
private handleWindowBlur;
|
|
60
|
+
get hasDragMoved(): boolean;
|
|
61
|
+
set hasDragMoved(value: boolean);
|
|
62
|
+
get activePointerOperation(): TrackPointerOperation | null;
|
|
63
|
+
set activePointerOperation(value: TrackPointerOperation | null);
|
|
64
|
+
get originalClipsState(): ClipType[];
|
|
65
|
+
set originalClipsState(value: ClipType[]);
|
|
66
|
+
get nonDraggedClips(): ClipType[];
|
|
67
|
+
set nonDraggedClips(value: ClipType[]);
|
|
68
|
+
get snapCandidateClips(): ClipType[];
|
|
69
|
+
set snapCandidateClips(value: ClipType[]);
|
|
70
|
+
get dragStartY(): number;
|
|
71
|
+
set dragStartY(value: number);
|
|
72
|
+
get dragTargetTrackY(): number;
|
|
73
|
+
set dragTargetTrackY(value: number);
|
|
74
|
+
get crossTrackDragOffsetY(): number;
|
|
75
|
+
set crossTrackDragOffsetY(value: number);
|
|
76
|
+
get crossTrackDragStartX(): number;
|
|
77
|
+
set crossTrackDragStartX(value: number);
|
|
78
|
+
get dragStartScrollLeft(): number;
|
|
79
|
+
set dragStartScrollLeft(value: number);
|
|
80
|
+
get dragGestureStartClientX(): number | null;
|
|
81
|
+
set dragGestureStartClientX(value: number | null);
|
|
82
|
+
get dragGestureStartClientY(): number | null;
|
|
83
|
+
set dragGestureStartClientY(value: number | null);
|
|
84
|
+
get lastDragClientX(): number | null;
|
|
85
|
+
set lastDragClientX(value: number | null);
|
|
86
|
+
get lastDragClientY(): number | null;
|
|
87
|
+
set lastDragClientY(value: number | null);
|
|
58
88
|
constructor(layer: Konva.Layer, config: TrackConfig, trackType: TrackType, zoom: number, trackY: number, trackHeight: number, theme: Theme, onClipUpdate: (clip: ClipType, originalClip?: ClipType, clipUpdates?: ClipStateUpdate[]) => void, onClipAdd: (clip: ClipType) => void, onClipRemove: (clipId: string) => void, onClipSplit: (clip1: ClipType, clip2: ClipType) => void, onClipSelect: (clip: ClipType) => void, onTimeJump: (time: TimeMs) => void, onHorizontalDragAutoScroll?: (nextScrollLeft: number) => number, onClipOverlap?: (clip: ClipType, currentTrackId: string) => void, onClipCrossTrackPreview?: (clip: ClipType, targetTrackY: number, currentTrackId: string) => 'self' | 'external' | 'clear', onClipCrossTrack?: (clip: ClipType, originalClip: ClipType | null, targetTrackY: number, currentTrackId: string) => boolean | void, onClearDropPreview?: () => void, onClearSelection?: () => void, onSnapGuideChange?: (guideTime: TimeMs | null) => void, onClipToggleSelection?: (clipId: string) => void, onSetSingleSelection?: (clipId: string) => void, getMultiDragClipIds?: (clipId: string) => string[] | null, onMultiDragMove?: (request: MultiDragMoveRequest) => boolean | void, onMultiDragInteractionEnd?: () => void, dragActivationThreshold?: number, enableClipSnap?: boolean, clipSnapThreshold?: number);
|
|
59
89
|
private initClips;
|
|
90
|
+
private getLegacyInteractionSnapshot;
|
|
60
91
|
private ensureDropPreviewGroup;
|
|
61
92
|
private createDropPreviewRect;
|
|
62
93
|
private showDropPreview;
|
|
@@ -107,10 +138,9 @@ export declare class Track {
|
|
|
107
138
|
render(shouldBatchDraw?: boolean): void;
|
|
108
139
|
private getTrackBackgroundFill;
|
|
109
140
|
getClips(): ClipType[];
|
|
110
|
-
|
|
141
|
+
getPrimarySelectedClip(): ClipType | null;
|
|
111
142
|
clearSelection(): void;
|
|
112
|
-
|
|
113
|
-
selectClip(clipId: string): void;
|
|
143
|
+
setSelection(clipIds: string[], preferredClipId?: string | null): void;
|
|
114
144
|
private applySelectionVisual;
|
|
115
145
|
private syncSelectionState;
|
|
116
146
|
splitSelectedClip(time: TimeMs): void;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ClipConfig, PlayState, PreviewAspectRatio, Track, TrackInsertionPlacement, TrackType } from '../models/types';
|
|
2
2
|
export interface DemoPreviewClipSnapshot extends ClipConfig {
|
|
3
3
|
id: string;
|
|
4
4
|
previewAutoOrder?: number;
|
|
@@ -17,7 +17,8 @@ export interface DemoPreviewRebuildSnapshot {
|
|
|
17
17
|
zoom: number;
|
|
18
18
|
playState: PlayState;
|
|
19
19
|
previewAspectRatio: PreviewAspectRatio;
|
|
20
|
-
|
|
20
|
+
selectedClipIds: string[];
|
|
21
|
+
selectedClipId?: string | null;
|
|
21
22
|
isPreviewAttached: boolean;
|
|
22
23
|
tracks: DemoPreviewTrackSnapshot[];
|
|
23
24
|
}
|
|
@@ -27,7 +28,7 @@ interface DemoPreviewStateReader {
|
|
|
27
28
|
getSpeed(): number;
|
|
28
29
|
getZoom(): number;
|
|
29
30
|
getPlayState(): PlayState;
|
|
30
|
-
|
|
31
|
+
getSelectedClipIds(): string[];
|
|
31
32
|
getTracks(): Track[];
|
|
32
33
|
}
|
|
33
34
|
interface DemoPreviewStateWriter extends DemoPreviewStateReader {
|
|
@@ -40,7 +41,7 @@ interface DemoPreviewStateWriter extends DemoPreviewStateReader {
|
|
|
40
41
|
play(): void;
|
|
41
42
|
renameTrack(trackId: string, newName: string): boolean;
|
|
42
43
|
resetPreviewAspectRatioToAuto(): void;
|
|
43
|
-
|
|
44
|
+
setSelection(clipIds: string[]): void;
|
|
44
45
|
setCurrentTime(time: number): void;
|
|
45
46
|
setPreviewAspectRatio(aspectRatio: {
|
|
46
47
|
width: number;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type { TimelinePreviewBackend } from './previewBackend';
|
|
1
|
+
import type { TimelinePreviewBackend, TimelinePreviewBackendOptions } from './previewBackend';
|
|
2
2
|
import { TimelinePreviewSession } from './timelinePreviewSession';
|
|
3
3
|
export declare class DomPreviewBackend extends TimelinePreviewSession implements TimelinePreviewBackend {
|
|
4
|
+
constructor(options?: TimelinePreviewBackendOptions);
|
|
4
5
|
destroy(): void;
|
|
5
6
|
}
|
|
@@ -8,6 +8,8 @@ export * from './timelineDurationController';
|
|
|
8
8
|
export { TimelineEventDispatcher } from './timelineEventDispatcher';
|
|
9
9
|
export { TimelinePlaybackResolver } from './timelinePlaybackResolver';
|
|
10
10
|
export { TimelinePreviewSession } from './timelinePreviewSession';
|
|
11
|
+
export { TimelinePreviewRuntimeController } from './timelinePreviewRuntimeController';
|
|
12
|
+
export { TimelinePreviewStateController, type TimelinePendingPreviewState } from './timelinePreviewStateController';
|
|
11
13
|
export * from './timelineSelectionController';
|
|
12
14
|
export * from './timelineTrackMutationController';
|
|
13
15
|
export { TimelineTrackInfoPanelController } from './timelineTrackInfoPanelController';
|
|
@@ -7,13 +7,14 @@ export interface TimelinePreviewSyncPayload {
|
|
|
7
7
|
currentTime: TimeMs;
|
|
8
8
|
playState: PlayState;
|
|
9
9
|
speed: number;
|
|
10
|
-
|
|
10
|
+
primarySelectedClipId?: string | null;
|
|
11
11
|
syncRequestId?: number;
|
|
12
12
|
}
|
|
13
13
|
export interface TimelinePreviewBackendCallbacks {
|
|
14
14
|
onBufferingStateChange?: (isBuffering: boolean) => void;
|
|
15
15
|
onSourceLoadingChange?: (pending: number) => void;
|
|
16
16
|
onSyncProcessed?: (syncRequestId?: number) => void;
|
|
17
|
+
onRuntimeStateChange?: (state: PreviewRuntimeState) => void;
|
|
17
18
|
onAspectRatioChange?: (aspectRatio: PreviewAspectRatio) => void;
|
|
18
19
|
onVisualTransformCommit?: (clipId: string, visualTransform: ClipVisualTransform) => void;
|
|
19
20
|
onPendingPreviewRetry?: () => void;
|
|
@@ -26,6 +27,27 @@ export interface PreviewPendingState {
|
|
|
26
27
|
isBuffering: boolean;
|
|
27
28
|
errorMessage: string | null;
|
|
28
29
|
}
|
|
30
|
+
export type PreviewRuntimePhase = 'idle' | 'awaiting-sync' | 'awaiting-media' | 'ready' | 'failed';
|
|
31
|
+
export type PreviewSlotPhase = 'idle' | 'binding' | 'primed' | 'active' | 'recovering' | 'failed';
|
|
32
|
+
export interface PreviewSlotDiagnostic {
|
|
33
|
+
trackId: string;
|
|
34
|
+
role: 'current' | 'preload';
|
|
35
|
+
kind: ActiveClipPlaybackInfo['trackType'];
|
|
36
|
+
clipId: string | null;
|
|
37
|
+
desiredSource: string | null;
|
|
38
|
+
actualSource: string | null;
|
|
39
|
+
phase: PreviewSlotPhase;
|
|
40
|
+
retryCount: number;
|
|
41
|
+
errorMessage: string | null;
|
|
42
|
+
}
|
|
43
|
+
export interface PreviewRuntimeState {
|
|
44
|
+
syncRequestId?: number;
|
|
45
|
+
phase: PreviewRuntimePhase;
|
|
46
|
+
loadingCount: number;
|
|
47
|
+
isBuffering: boolean;
|
|
48
|
+
errorMessage: string | null;
|
|
49
|
+
slots: PreviewSlotDiagnostic[];
|
|
50
|
+
}
|
|
29
51
|
export interface TimelinePreviewBackend {
|
|
30
52
|
attach(container: HTMLElement): void;
|
|
31
53
|
detach(): void;
|
|
@@ -2,7 +2,7 @@ import { Theme, Clip } from '../models';
|
|
|
2
2
|
export interface TimelineClipConfigControllerConfig {
|
|
3
3
|
container: HTMLElement;
|
|
4
4
|
theme: Theme;
|
|
5
|
-
|
|
5
|
+
getPrimarySelectedClip: () => Clip | null;
|
|
6
6
|
updateClip: (clipId: string, updates: Partial<Clip>) => void;
|
|
7
7
|
}
|
|
8
8
|
export declare class TimelineClipConfigController {
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { PreviewAspectRatio, PreviewBackendType } from '../models/types';
|
|
2
|
+
import type { TimelinePreviewBackend, TimelinePreviewBackendCallbacks } from './previewBackend';
|
|
3
|
+
interface TimelinePreviewRuntimeControllerOptions {
|
|
4
|
+
createBackendCallbacks: (callbackToken: number) => TimelinePreviewBackendCallbacks;
|
|
5
|
+
createBackend: (resolvedBackend: Exclude<PreviewBackendType, 'auto'>, callbacks: TimelinePreviewBackendCallbacks) => TimelinePreviewBackend;
|
|
6
|
+
resolveConfiguredBackend: (runtimeOverride: Exclude<PreviewBackendType, 'auto'> | null) => Exclude<PreviewBackendType, 'auto'>;
|
|
7
|
+
}
|
|
8
|
+
export declare class TimelinePreviewRuntimeController {
|
|
9
|
+
private readonly options;
|
|
10
|
+
private _previewSession?;
|
|
11
|
+
private _previewMountContainer;
|
|
12
|
+
private _resolvedPreviewBackend;
|
|
13
|
+
private _runtimePreviewBackendOverride;
|
|
14
|
+
private _activePreviewCallbackToken;
|
|
15
|
+
constructor(options: TimelinePreviewRuntimeControllerOptions);
|
|
16
|
+
get previewSession(): TimelinePreviewBackend | undefined;
|
|
17
|
+
set previewSession(value: TimelinePreviewBackend | undefined);
|
|
18
|
+
get previewMountContainer(): HTMLElement | null;
|
|
19
|
+
set previewMountContainer(value: HTMLElement | null);
|
|
20
|
+
get resolvedPreviewBackend(): Exclude<PreviewBackendType, 'auto'>;
|
|
21
|
+
set resolvedPreviewBackend(value: Exclude<PreviewBackendType, 'auto'>);
|
|
22
|
+
get runtimePreviewBackendOverride(): Exclude<PreviewBackendType, 'auto'> | null;
|
|
23
|
+
set runtimePreviewBackendOverride(value: Exclude<PreviewBackendType, 'auto'> | null);
|
|
24
|
+
get activePreviewCallbackToken(): number;
|
|
25
|
+
set activePreviewCallbackToken(value: number);
|
|
26
|
+
isActiveCallbackToken(callbackToken: number): boolean;
|
|
27
|
+
resolveConfiguredPreviewBackend(): Exclude<PreviewBackendType, 'auto'>;
|
|
28
|
+
createPreviewBackend(resolvedBackend?: Exclude<PreviewBackendType, 'auto'>): TimelinePreviewBackend;
|
|
29
|
+
getPreviewSession(): TimelinePreviewBackend;
|
|
30
|
+
hasAttachedPreview(): boolean;
|
|
31
|
+
attachPreview(container: HTMLElement, aspectRatio: PreviewAspectRatio): PreviewAspectRatio;
|
|
32
|
+
detachPreviewSession(): boolean;
|
|
33
|
+
destroyPreviewSession(clearReference?: boolean): void;
|
|
34
|
+
recreateDetachedPreviewSession(): void;
|
|
35
|
+
fallbackToDom(aspectRatio: PreviewAspectRatio): {
|
|
36
|
+
handled: boolean;
|
|
37
|
+
aspectRatio: PreviewAspectRatio;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export {};
|