@webpacked-timeline/core 1.0.0-beta.1
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/CHANGELOG.md +36 -0
- package/LICENSE +21 -0
- package/README.md +162 -0
- package/dist/chunk-27XCNVPR.js +5969 -0
- package/dist/chunk-6PDBJDHM.js +2263 -0
- package/dist/chunk-BWPS6NQT.js +7465 -0
- package/dist/chunk-FBOYSUYV.js +1280 -0
- package/dist/chunk-FR632TZX.js +1870 -0
- package/dist/chunk-HW4Z7YLJ.js +1242 -0
- package/dist/chunk-HWW62IFH.js +5424 -0
- package/dist/chunk-I2GZXRH4.js +4790 -0
- package/dist/chunk-JQZE3OK4.js +1255 -0
- package/dist/chunk-KF7JNK2F.js +1864 -0
- package/dist/chunk-KR3P2DYK.js +5655 -0
- package/dist/chunk-MO5DSFSW.js +2214 -0
- package/dist/chunk-MQAW33RJ.js +5530 -0
- package/dist/chunk-N4WUWZZX.js +2833 -0
- package/dist/chunk-NRJV7I4C.js +1331 -0
- package/dist/chunk-NXG52532.js +2230 -0
- package/dist/chunk-PVXF67CN.js +1278 -0
- package/dist/chunk-QSB6DHIF.js +5429 -0
- package/dist/chunk-QYWJT7HR.js +5837 -0
- package/dist/chunk-SWBRCMW7.js +7466 -0
- package/dist/chunk-TAT3ULSV.js +2214 -0
- package/dist/chunk-TTDP5JUM.js +2228 -0
- package/dist/chunk-UAGP4VPG.js +1739 -0
- package/dist/chunk-WIG6SY7A.js +1183 -0
- package/dist/chunk-YJ2K5N2R.js +6187 -0
- package/dist/index-3Lr_vKBd.d.cts +2810 -0
- package/dist/index-3Lr_vKBd.d.ts +2810 -0
- package/dist/index-7IPJn1yM.d.cts +1146 -0
- package/dist/index-7IPJn1yM.d.ts +1146 -0
- package/dist/index-B0xOv0V0.d.cts +3259 -0
- package/dist/index-B0xOv0V0.d.ts +3259 -0
- package/dist/index-B2m3zwg7.d.cts +1381 -0
- package/dist/index-B2m3zwg7.d.ts +1381 -0
- package/dist/index-B3sUrU_X.d.cts +1249 -0
- package/dist/index-B3sUrU_X.d.ts +1249 -0
- package/dist/index-B6wla7ZJ.d.cts +2751 -0
- package/dist/index-B6wla7ZJ.d.ts +2751 -0
- package/dist/index-BIv8RWWT.d.cts +1574 -0
- package/dist/index-BIv8RWWT.d.ts +1574 -0
- package/dist/index-BJv6hDHL.d.cts +3255 -0
- package/dist/index-BJv6hDHL.d.ts +3255 -0
- package/dist/index-BUCimS2e.d.cts +1393 -0
- package/dist/index-BUCimS2e.d.ts +1393 -0
- package/dist/index-Bw_nvNcG.d.cts +1275 -0
- package/dist/index-Bw_nvNcG.d.ts +1275 -0
- package/dist/index-ByG0gOtd.d.cts +1167 -0
- package/dist/index-ByG0gOtd.d.ts +1167 -0
- package/dist/index-CDGd2XXv.d.cts +2492 -0
- package/dist/index-CDGd2XXv.d.ts +2492 -0
- package/dist/index-CznAVeJ6.d.cts +1145 -0
- package/dist/index-CznAVeJ6.d.ts +1145 -0
- package/dist/index-DQD9IMh7.d.cts +2534 -0
- package/dist/index-DQD9IMh7.d.ts +2534 -0
- package/dist/index-Dl3qtJEI.d.cts +2178 -0
- package/dist/index-Dl3qtJEI.d.ts +2178 -0
- package/dist/index-DnE2A-Nz.d.cts +2603 -0
- package/dist/index-DnE2A-Nz.d.ts +2603 -0
- package/dist/index-DrOA6QmW.d.cts +2492 -0
- package/dist/index-DrOA6QmW.d.ts +2492 -0
- package/dist/index-Vpa3rPEM.d.cts +1402 -0
- package/dist/index-Vpa3rPEM.d.ts +1402 -0
- package/dist/index-jP6BomSd.d.cts +2640 -0
- package/dist/index-jP6BomSd.d.ts +2640 -0
- package/dist/index-wiGRwVyY.d.cts +3259 -0
- package/dist/index-wiGRwVyY.d.ts +3259 -0
- package/dist/index.cjs +7386 -0
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +263 -0
- package/dist/internal.cjs +7721 -0
- package/dist/internal.d.cts +704 -0
- package/dist/internal.d.ts +704 -0
- package/dist/internal.js +405 -0
- package/package.json +58 -0
|
@@ -0,0 +1,1739 @@
|
|
|
1
|
+
// src/types/clip.ts
|
|
2
|
+
function createClip(params) {
|
|
3
|
+
const clip = {
|
|
4
|
+
id: params.id,
|
|
5
|
+
assetId: params.assetId,
|
|
6
|
+
trackId: params.trackId,
|
|
7
|
+
timelineStart: params.timelineStart,
|
|
8
|
+
timelineEnd: params.timelineEnd,
|
|
9
|
+
mediaIn: params.mediaIn,
|
|
10
|
+
mediaOut: params.mediaOut
|
|
11
|
+
};
|
|
12
|
+
if (params.label !== void 0) {
|
|
13
|
+
clip.label = params.label;
|
|
14
|
+
}
|
|
15
|
+
if (params.metadata !== void 0) {
|
|
16
|
+
clip.metadata = params.metadata;
|
|
17
|
+
}
|
|
18
|
+
return clip;
|
|
19
|
+
}
|
|
20
|
+
function getClipDuration(clip) {
|
|
21
|
+
return clip.timelineEnd - clip.timelineStart;
|
|
22
|
+
}
|
|
23
|
+
function getClipMediaDuration(clip) {
|
|
24
|
+
return clip.mediaOut - clip.mediaIn;
|
|
25
|
+
}
|
|
26
|
+
function clipContainsFrame(clip, frame2) {
|
|
27
|
+
return frame2 >= clip.timelineStart && frame2 < clip.timelineEnd;
|
|
28
|
+
}
|
|
29
|
+
function clipsOverlap(clip1, clip2) {
|
|
30
|
+
return clip1.timelineStart < clip2.timelineEnd && clip2.timelineStart < clip1.timelineEnd;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// src/types/validation.ts
|
|
34
|
+
function validResult() {
|
|
35
|
+
return {
|
|
36
|
+
valid: true,
|
|
37
|
+
errors: []
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function invalidResult(code, message, context) {
|
|
41
|
+
const error = { code, message };
|
|
42
|
+
if (context !== void 0) {
|
|
43
|
+
error.context = context;
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
valid: false,
|
|
47
|
+
errors: [error]
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function invalidResults(errors) {
|
|
51
|
+
return {
|
|
52
|
+
valid: false,
|
|
53
|
+
errors
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function combineResults(...results) {
|
|
57
|
+
const allErrors = [];
|
|
58
|
+
for (const result of results) {
|
|
59
|
+
if (!result.valid) {
|
|
60
|
+
allErrors.push(...result.errors);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (allErrors.length > 0) {
|
|
64
|
+
return invalidResults(allErrors);
|
|
65
|
+
}
|
|
66
|
+
return validResult();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// src/systems/asset-registry.ts
|
|
70
|
+
function registerAsset(state, asset) {
|
|
71
|
+
const newAssets = new Map(state.assets);
|
|
72
|
+
newAssets.set(asset.id, asset);
|
|
73
|
+
return {
|
|
74
|
+
...state,
|
|
75
|
+
assets: newAssets
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function getAsset(state, assetId) {
|
|
79
|
+
return state.assets.get(assetId);
|
|
80
|
+
}
|
|
81
|
+
function hasAsset(state, assetId) {
|
|
82
|
+
return state.assets.has(assetId);
|
|
83
|
+
}
|
|
84
|
+
function getAllAssets(state) {
|
|
85
|
+
return Array.from(state.assets.values());
|
|
86
|
+
}
|
|
87
|
+
function unregisterAsset(state, assetId) {
|
|
88
|
+
const newAssets = new Map(state.assets);
|
|
89
|
+
newAssets.delete(assetId);
|
|
90
|
+
return {
|
|
91
|
+
...state,
|
|
92
|
+
assets: newAssets
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// src/systems/validation.ts
|
|
97
|
+
function validateClip(state, clip) {
|
|
98
|
+
const errors = [];
|
|
99
|
+
const asset = getAsset(state, clip.assetId);
|
|
100
|
+
if (!asset) {
|
|
101
|
+
errors.push(invalidResult(
|
|
102
|
+
"ASSET_NOT_FOUND",
|
|
103
|
+
`Asset '${clip.assetId}' not found in registry`,
|
|
104
|
+
{ clipId: clip.id, assetId: clip.assetId }
|
|
105
|
+
));
|
|
106
|
+
return combineResults(...errors);
|
|
107
|
+
}
|
|
108
|
+
if (clip.timelineEnd <= clip.timelineStart) {
|
|
109
|
+
errors.push(invalidResult(
|
|
110
|
+
"INVALID_TIMELINE_BOUNDS",
|
|
111
|
+
`Clip timeline end (${clip.timelineEnd}) must be greater than start (${clip.timelineStart})`,
|
|
112
|
+
{ clipId: clip.id, timelineStart: clip.timelineStart, timelineEnd: clip.timelineEnd }
|
|
113
|
+
));
|
|
114
|
+
}
|
|
115
|
+
if (clip.mediaIn < 0) {
|
|
116
|
+
errors.push(invalidResult(
|
|
117
|
+
"INVALID_MEDIA_IN",
|
|
118
|
+
`Clip media in (${clip.mediaIn}) must be >= 0`,
|
|
119
|
+
{ clipId: clip.id, mediaIn: clip.mediaIn }
|
|
120
|
+
));
|
|
121
|
+
}
|
|
122
|
+
if (clip.mediaOut <= clip.mediaIn) {
|
|
123
|
+
errors.push(invalidResult(
|
|
124
|
+
"INVALID_MEDIA_BOUNDS",
|
|
125
|
+
`Clip media out (${clip.mediaOut}) must be greater than media in (${clip.mediaIn})`,
|
|
126
|
+
{ clipId: clip.id, mediaIn: clip.mediaIn, mediaOut: clip.mediaOut }
|
|
127
|
+
));
|
|
128
|
+
}
|
|
129
|
+
if (clip.mediaOut > asset.duration) {
|
|
130
|
+
errors.push(invalidResult(
|
|
131
|
+
"MEDIA_EXCEEDS_ASSET",
|
|
132
|
+
`Clip media out (${clip.mediaOut}) exceeds asset duration (${asset.duration})`,
|
|
133
|
+
{ clipId: clip.id, mediaOut: clip.mediaOut, assetDuration: asset.duration }
|
|
134
|
+
));
|
|
135
|
+
}
|
|
136
|
+
const timelineDuration = getClipDuration(clip);
|
|
137
|
+
const mediaDuration = getClipMediaDuration(clip);
|
|
138
|
+
if (timelineDuration !== mediaDuration) {
|
|
139
|
+
errors.push(invalidResult(
|
|
140
|
+
"DURATION_MISMATCH",
|
|
141
|
+
`Clip timeline duration (${timelineDuration}) must match media duration (${mediaDuration}) in Phase 1`,
|
|
142
|
+
{ clipId: clip.id, timelineDuration, mediaDuration }
|
|
143
|
+
));
|
|
144
|
+
}
|
|
145
|
+
return combineResults(...errors);
|
|
146
|
+
}
|
|
147
|
+
function validateTrack(state, track) {
|
|
148
|
+
const errors = [];
|
|
149
|
+
for (const clip of track.clips) {
|
|
150
|
+
const clipResult = validateClip(state, clip);
|
|
151
|
+
if (!clipResult.valid) {
|
|
152
|
+
errors.push(clipResult);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
for (let i = 0; i < track.clips.length; i++) {
|
|
156
|
+
for (let j = i + 1; j < track.clips.length; j++) {
|
|
157
|
+
const clip1 = track.clips[i];
|
|
158
|
+
const clip2 = track.clips[j];
|
|
159
|
+
if (!clip1 || !clip2) {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (clipsOverlap(clip1, clip2)) {
|
|
163
|
+
errors.push(invalidResult(
|
|
164
|
+
"CLIPS_OVERLAP",
|
|
165
|
+
`Clips '${clip1.id}' and '${clip2.id}' overlap on track '${track.id}'`,
|
|
166
|
+
{
|
|
167
|
+
trackId: track.id,
|
|
168
|
+
clip1Id: clip1.id,
|
|
169
|
+
clip2Id: clip2.id,
|
|
170
|
+
clip1Start: clip1.timelineStart,
|
|
171
|
+
clip1End: clip1.timelineEnd,
|
|
172
|
+
clip2Start: clip2.timelineStart,
|
|
173
|
+
clip2End: clip2.timelineEnd
|
|
174
|
+
}
|
|
175
|
+
));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return combineResults(...errors);
|
|
180
|
+
}
|
|
181
|
+
function validateTimeline(state) {
|
|
182
|
+
const errors = [];
|
|
183
|
+
if (state.timeline.fps <= 0) {
|
|
184
|
+
errors.push(invalidResult(
|
|
185
|
+
"INVALID_FPS",
|
|
186
|
+
`Timeline FPS must be positive, got ${state.timeline.fps}`,
|
|
187
|
+
{ fps: state.timeline.fps }
|
|
188
|
+
));
|
|
189
|
+
}
|
|
190
|
+
if (state.timeline.duration <= 0) {
|
|
191
|
+
errors.push(invalidResult(
|
|
192
|
+
"INVALID_DURATION",
|
|
193
|
+
`Timeline duration must be positive, got ${state.timeline.duration}`,
|
|
194
|
+
{ duration: state.timeline.duration }
|
|
195
|
+
));
|
|
196
|
+
}
|
|
197
|
+
for (const track of state.timeline.tracks) {
|
|
198
|
+
const trackResult = validateTrack(state, track);
|
|
199
|
+
if (!trackResult.valid) {
|
|
200
|
+
errors.push(trackResult);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return combineResults(...errors);
|
|
204
|
+
}
|
|
205
|
+
function validateNoOverlap(track, clip) {
|
|
206
|
+
for (const existingClip of track.clips) {
|
|
207
|
+
if (existingClip.id === clip.id) {
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
if (clipsOverlap(existingClip, clip)) {
|
|
211
|
+
return invalidResult(
|
|
212
|
+
"CLIPS_OVERLAP",
|
|
213
|
+
`Clip '${clip.id}' would overlap with existing clip '${existingClip.id}' on track '${track.id}'`,
|
|
214
|
+
{
|
|
215
|
+
trackId: track.id,
|
|
216
|
+
newClipId: clip.id,
|
|
217
|
+
existingClipId: existingClip.id,
|
|
218
|
+
newClipStart: clip.timelineStart,
|
|
219
|
+
newClipEnd: clip.timelineEnd,
|
|
220
|
+
existingClipStart: existingClip.timelineStart,
|
|
221
|
+
existingClipEnd: existingClip.timelineEnd
|
|
222
|
+
}
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return validResult();
|
|
227
|
+
}
|
|
228
|
+
function validateTrackTypeMatch(state, clip, targetTrack) {
|
|
229
|
+
const asset = getAsset(state, clip.assetId);
|
|
230
|
+
if (!asset) {
|
|
231
|
+
return invalidResult(
|
|
232
|
+
"ASSET_NOT_FOUND",
|
|
233
|
+
`Asset '${clip.assetId}' not found in registry`,
|
|
234
|
+
{ clipId: clip.id, assetId: clip.assetId }
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
if (asset.type === "video" && targetTrack.type !== "video") {
|
|
238
|
+
return invalidResult(
|
|
239
|
+
"TRACK_TYPE_MISMATCH",
|
|
240
|
+
`Cannot place video clip '${clip.id}' on ${targetTrack.type} track '${targetTrack.id}'`,
|
|
241
|
+
{
|
|
242
|
+
clipId: clip.id,
|
|
243
|
+
assetType: asset.type,
|
|
244
|
+
trackType: targetTrack.type,
|
|
245
|
+
trackId: targetTrack.id
|
|
246
|
+
}
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
if (asset.type === "audio" && targetTrack.type !== "audio") {
|
|
250
|
+
return invalidResult(
|
|
251
|
+
"TRACK_TYPE_MISMATCH",
|
|
252
|
+
`Cannot place audio clip '${clip.id}' on ${targetTrack.type} track '${targetTrack.id}'`,
|
|
253
|
+
{
|
|
254
|
+
clipId: clip.id,
|
|
255
|
+
assetType: asset.type,
|
|
256
|
+
trackType: targetTrack.type,
|
|
257
|
+
trackId: targetTrack.id
|
|
258
|
+
}
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
if (asset.type === "image" && targetTrack.type !== "video") {
|
|
262
|
+
return invalidResult(
|
|
263
|
+
"TRACK_TYPE_MISMATCH",
|
|
264
|
+
`Cannot place image clip '${clip.id}' on ${targetTrack.type} track '${targetTrack.id}'`,
|
|
265
|
+
{
|
|
266
|
+
clipId: clip.id,
|
|
267
|
+
assetType: asset.type,
|
|
268
|
+
trackType: targetTrack.type,
|
|
269
|
+
trackId: targetTrack.id
|
|
270
|
+
}
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
return validResult();
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// src/systems/queries.ts
|
|
277
|
+
function findClipById(state, clipId) {
|
|
278
|
+
for (const track of state.timeline.tracks) {
|
|
279
|
+
const clip = track.clips.find((c) => c.id === clipId);
|
|
280
|
+
if (clip) {
|
|
281
|
+
return clip;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return void 0;
|
|
285
|
+
}
|
|
286
|
+
function findTrackById(state, trackId) {
|
|
287
|
+
return state.timeline.tracks.find((t) => t.id === trackId);
|
|
288
|
+
}
|
|
289
|
+
function getClipsOnTrack(state, trackId) {
|
|
290
|
+
const track = findTrackById(state, trackId);
|
|
291
|
+
return track ? track.clips : [];
|
|
292
|
+
}
|
|
293
|
+
function getClipsAtFrame(state, frame2) {
|
|
294
|
+
const clips = [];
|
|
295
|
+
for (const track of state.timeline.tracks) {
|
|
296
|
+
for (const clip of track.clips) {
|
|
297
|
+
if (clipContainsFrame(clip, frame2)) {
|
|
298
|
+
clips.push(clip);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return clips;
|
|
303
|
+
}
|
|
304
|
+
function getClipsInRange(state, start, end) {
|
|
305
|
+
const clips = [];
|
|
306
|
+
for (const track of state.timeline.tracks) {
|
|
307
|
+
for (const clip of track.clips) {
|
|
308
|
+
if (clip.timelineStart < end && clip.timelineEnd > start) {
|
|
309
|
+
clips.push(clip);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return clips;
|
|
314
|
+
}
|
|
315
|
+
function getAllClips(state) {
|
|
316
|
+
const clips = [];
|
|
317
|
+
for (const track of state.timeline.tracks) {
|
|
318
|
+
clips.push(...track.clips);
|
|
319
|
+
}
|
|
320
|
+
return clips;
|
|
321
|
+
}
|
|
322
|
+
function getAllTracks(state) {
|
|
323
|
+
return state.timeline.tracks;
|
|
324
|
+
}
|
|
325
|
+
function findTrackIndex(state, trackId) {
|
|
326
|
+
return state.timeline.tracks.findIndex((t) => t.id === trackId);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// src/types/track.ts
|
|
330
|
+
function createTrack(params) {
|
|
331
|
+
const track = {
|
|
332
|
+
id: params.id,
|
|
333
|
+
name: params.name,
|
|
334
|
+
type: params.type,
|
|
335
|
+
clips: params.clips ?? [],
|
|
336
|
+
locked: params.locked ?? false,
|
|
337
|
+
muted: params.muted ?? false,
|
|
338
|
+
solo: params.solo ?? false,
|
|
339
|
+
height: params.height ?? 56
|
|
340
|
+
};
|
|
341
|
+
if (params.metadata !== void 0) {
|
|
342
|
+
track.metadata = params.metadata;
|
|
343
|
+
}
|
|
344
|
+
return track;
|
|
345
|
+
}
|
|
346
|
+
function sortTrackClips(track) {
|
|
347
|
+
return {
|
|
348
|
+
...track,
|
|
349
|
+
clips: [...track.clips].sort((a, b) => a.timelineStart - b.timelineStart)
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// src/operations/clip-operations.ts
|
|
354
|
+
function addClip(state, trackId, clip) {
|
|
355
|
+
const trackIndex = state.timeline.tracks.findIndex((t) => t.id === trackId);
|
|
356
|
+
if (trackIndex === -1) {
|
|
357
|
+
return state;
|
|
358
|
+
}
|
|
359
|
+
const track = state.timeline.tracks[trackIndex];
|
|
360
|
+
if (!track) {
|
|
361
|
+
return state;
|
|
362
|
+
}
|
|
363
|
+
const newTrack = sortTrackClips({
|
|
364
|
+
...track,
|
|
365
|
+
clips: [...track.clips, clip]
|
|
366
|
+
});
|
|
367
|
+
const newTracks = [...state.timeline.tracks];
|
|
368
|
+
newTracks[trackIndex] = newTrack;
|
|
369
|
+
return {
|
|
370
|
+
...state,
|
|
371
|
+
timeline: {
|
|
372
|
+
...state.timeline,
|
|
373
|
+
tracks: newTracks
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
function removeClip(state, clipId) {
|
|
378
|
+
const newTracks = state.timeline.tracks.map((track) => ({
|
|
379
|
+
...track,
|
|
380
|
+
clips: track.clips.filter((c) => c.id !== clipId)
|
|
381
|
+
}));
|
|
382
|
+
return {
|
|
383
|
+
...state,
|
|
384
|
+
timeline: {
|
|
385
|
+
...state.timeline,
|
|
386
|
+
tracks: newTracks
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
function moveClip(state, clipId, newStart) {
|
|
391
|
+
const clip = findClipById(state, clipId);
|
|
392
|
+
if (!clip) {
|
|
393
|
+
return state;
|
|
394
|
+
}
|
|
395
|
+
const duration = clip.timelineEnd - clip.timelineStart;
|
|
396
|
+
const newEnd = newStart + duration;
|
|
397
|
+
return updateClip(state, clipId, {
|
|
398
|
+
timelineStart: newStart,
|
|
399
|
+
timelineEnd: newEnd
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
function resizeClip(state, clipId, newStart, newEnd) {
|
|
403
|
+
const clip = findClipById(state, clipId);
|
|
404
|
+
if (!clip) {
|
|
405
|
+
return state;
|
|
406
|
+
}
|
|
407
|
+
const startDelta = newStart - clip.timelineStart;
|
|
408
|
+
const endDelta = newEnd - clip.timelineEnd;
|
|
409
|
+
let newMediaIn = clip.mediaIn;
|
|
410
|
+
let newMediaOut = clip.mediaOut;
|
|
411
|
+
if (startDelta !== 0) {
|
|
412
|
+
newMediaIn = clip.mediaIn + startDelta;
|
|
413
|
+
}
|
|
414
|
+
if (endDelta !== 0) {
|
|
415
|
+
newMediaOut = clip.mediaOut + endDelta;
|
|
416
|
+
}
|
|
417
|
+
return updateClip(state, clipId, {
|
|
418
|
+
timelineStart: newStart,
|
|
419
|
+
timelineEnd: newEnd,
|
|
420
|
+
mediaIn: newMediaIn,
|
|
421
|
+
mediaOut: newMediaOut
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
function trimClip(state, clipId, newMediaIn, newMediaOut) {
|
|
425
|
+
return updateClip(state, clipId, {
|
|
426
|
+
mediaIn: newMediaIn,
|
|
427
|
+
mediaOut: newMediaOut
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
function updateClip(state, clipId, updates) {
|
|
431
|
+
const newTracks = state.timeline.tracks.map((track) => {
|
|
432
|
+
const clipIndex = track.clips.findIndex((c) => c.id === clipId);
|
|
433
|
+
if (clipIndex === -1) {
|
|
434
|
+
return track;
|
|
435
|
+
}
|
|
436
|
+
const newClips = [...track.clips];
|
|
437
|
+
const existingClip = newClips[clipIndex];
|
|
438
|
+
if (!existingClip) {
|
|
439
|
+
return track;
|
|
440
|
+
}
|
|
441
|
+
newClips[clipIndex] = {
|
|
442
|
+
...existingClip,
|
|
443
|
+
...updates
|
|
444
|
+
};
|
|
445
|
+
return sortTrackClips({
|
|
446
|
+
...track,
|
|
447
|
+
clips: newClips
|
|
448
|
+
});
|
|
449
|
+
});
|
|
450
|
+
return {
|
|
451
|
+
...state,
|
|
452
|
+
timeline: {
|
|
453
|
+
...state.timeline,
|
|
454
|
+
tracks: newTracks
|
|
455
|
+
}
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
function moveClipToTrack(state, clipId, targetTrackId) {
|
|
459
|
+
const clip = findClipById(state, clipId);
|
|
460
|
+
if (!clip) {
|
|
461
|
+
return state;
|
|
462
|
+
}
|
|
463
|
+
const targetTrack = findTrackById(state, targetTrackId);
|
|
464
|
+
if (!targetTrack) {
|
|
465
|
+
return state;
|
|
466
|
+
}
|
|
467
|
+
const validationResult = validateTrackTypeMatch(state, clip, targetTrack);
|
|
468
|
+
if (!validationResult.valid) {
|
|
469
|
+
return state;
|
|
470
|
+
}
|
|
471
|
+
if (clip.trackId === targetTrackId) {
|
|
472
|
+
return state;
|
|
473
|
+
}
|
|
474
|
+
let newState = removeClip(state, clipId);
|
|
475
|
+
const updatedClip = { ...clip, trackId: targetTrackId };
|
|
476
|
+
newState = addClip(newState, targetTrackId, updatedClip);
|
|
477
|
+
return newState;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// src/operations/track-operations.ts
|
|
481
|
+
function addTrack(state, track) {
|
|
482
|
+
return {
|
|
483
|
+
...state,
|
|
484
|
+
timeline: {
|
|
485
|
+
...state.timeline,
|
|
486
|
+
tracks: [...state.timeline.tracks, track]
|
|
487
|
+
}
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
function removeTrack(state, trackId) {
|
|
491
|
+
return {
|
|
492
|
+
...state,
|
|
493
|
+
timeline: {
|
|
494
|
+
...state.timeline,
|
|
495
|
+
tracks: state.timeline.tracks.filter((t) => t.id !== trackId)
|
|
496
|
+
}
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
function moveTrack(state, trackId, newIndex) {
|
|
500
|
+
const currentIndex = findTrackIndex(state, trackId);
|
|
501
|
+
if (currentIndex === -1) {
|
|
502
|
+
return state;
|
|
503
|
+
}
|
|
504
|
+
const newTracks = [...state.timeline.tracks];
|
|
505
|
+
const [track] = newTracks.splice(currentIndex, 1);
|
|
506
|
+
if (!track) {
|
|
507
|
+
return state;
|
|
508
|
+
}
|
|
509
|
+
newTracks.splice(newIndex, 0, track);
|
|
510
|
+
return {
|
|
511
|
+
...state,
|
|
512
|
+
timeline: {
|
|
513
|
+
...state.timeline,
|
|
514
|
+
tracks: newTracks
|
|
515
|
+
}
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
function updateTrack(state, trackId, updates) {
|
|
519
|
+
const trackIndex = findTrackIndex(state, trackId);
|
|
520
|
+
if (trackIndex === -1) {
|
|
521
|
+
return state;
|
|
522
|
+
}
|
|
523
|
+
const newTracks = [...state.timeline.tracks];
|
|
524
|
+
const existingTrack = newTracks[trackIndex];
|
|
525
|
+
if (!existingTrack) {
|
|
526
|
+
return state;
|
|
527
|
+
}
|
|
528
|
+
newTracks[trackIndex] = {
|
|
529
|
+
...existingTrack,
|
|
530
|
+
...updates
|
|
531
|
+
};
|
|
532
|
+
return {
|
|
533
|
+
...state,
|
|
534
|
+
timeline: {
|
|
535
|
+
...state.timeline,
|
|
536
|
+
tracks: newTracks
|
|
537
|
+
}
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
function toggleTrackMute(state, trackId) {
|
|
541
|
+
const trackIndex = findTrackIndex(state, trackId);
|
|
542
|
+
if (trackIndex === -1) {
|
|
543
|
+
return state;
|
|
544
|
+
}
|
|
545
|
+
const track = state.timeline.tracks[trackIndex];
|
|
546
|
+
if (!track) {
|
|
547
|
+
return state;
|
|
548
|
+
}
|
|
549
|
+
return updateTrack(state, trackId, { muted: !track.muted });
|
|
550
|
+
}
|
|
551
|
+
function toggleTrackLock(state, trackId) {
|
|
552
|
+
const trackIndex = findTrackIndex(state, trackId);
|
|
553
|
+
if (trackIndex === -1) {
|
|
554
|
+
return state;
|
|
555
|
+
}
|
|
556
|
+
const track = state.timeline.tracks[trackIndex];
|
|
557
|
+
if (!track) {
|
|
558
|
+
return state;
|
|
559
|
+
}
|
|
560
|
+
return updateTrack(state, trackId, { locked: !track.locked });
|
|
561
|
+
}
|
|
562
|
+
function toggleTrackSolo(state, trackId) {
|
|
563
|
+
const trackIndex = findTrackIndex(state, trackId);
|
|
564
|
+
if (trackIndex === -1) {
|
|
565
|
+
return state;
|
|
566
|
+
}
|
|
567
|
+
const track = state.timeline.tracks[trackIndex];
|
|
568
|
+
if (!track) {
|
|
569
|
+
return state;
|
|
570
|
+
}
|
|
571
|
+
return updateTrack(state, trackId, { solo: !track.solo });
|
|
572
|
+
}
|
|
573
|
+
function setTrackHeight(state, trackId, height) {
|
|
574
|
+
return updateTrack(state, trackId, { height: Math.max(40, Math.min(200, height)) });
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// src/operations/timeline-operations.ts
|
|
578
|
+
function setTimelineDuration(state, duration) {
|
|
579
|
+
return {
|
|
580
|
+
...state,
|
|
581
|
+
timeline: {
|
|
582
|
+
...state.timeline,
|
|
583
|
+
duration
|
|
584
|
+
}
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
function setTimelineName(state, name) {
|
|
588
|
+
return {
|
|
589
|
+
...state,
|
|
590
|
+
timeline: {
|
|
591
|
+
...state.timeline,
|
|
592
|
+
name
|
|
593
|
+
}
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
function updateTimelineMetadata(state, metadata) {
|
|
597
|
+
return {
|
|
598
|
+
...state,
|
|
599
|
+
timeline: {
|
|
600
|
+
...state.timeline,
|
|
601
|
+
metadata: {
|
|
602
|
+
...state.timeline.metadata,
|
|
603
|
+
...metadata
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// src/engine/transactions.ts
|
|
610
|
+
function beginTransaction(state) {
|
|
611
|
+
return {
|
|
612
|
+
initialState: state,
|
|
613
|
+
currentState: state,
|
|
614
|
+
operations: [],
|
|
615
|
+
finalized: false
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
function applyOperation(tx, operation) {
|
|
619
|
+
if (tx.finalized) {
|
|
620
|
+
throw new Error("Cannot apply operation to finalized transaction");
|
|
621
|
+
}
|
|
622
|
+
const newState = operation(tx.currentState);
|
|
623
|
+
return {
|
|
624
|
+
...tx,
|
|
625
|
+
currentState: newState,
|
|
626
|
+
operations: [...tx.operations, operation]
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
function commitTransaction(tx) {
|
|
630
|
+
if (tx.finalized) {
|
|
631
|
+
throw new Error("Transaction already finalized");
|
|
632
|
+
}
|
|
633
|
+
tx.finalized = true;
|
|
634
|
+
return tx.currentState;
|
|
635
|
+
}
|
|
636
|
+
function rollbackTransaction(tx) {
|
|
637
|
+
if (tx.finalized) {
|
|
638
|
+
throw new Error("Transaction already finalized");
|
|
639
|
+
}
|
|
640
|
+
tx.finalized = true;
|
|
641
|
+
return tx.initialState;
|
|
642
|
+
}
|
|
643
|
+
function getOperationCount(tx) {
|
|
644
|
+
return tx.operations.length;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// src/operations/ripple.ts
|
|
648
|
+
function rippleDelete(state, clipId) {
|
|
649
|
+
const clip = findClipById(state, clipId);
|
|
650
|
+
if (!clip) {
|
|
651
|
+
throw new Error(`Clip not found: ${clipId}`);
|
|
652
|
+
}
|
|
653
|
+
const track = findTrackById(state, clip.trackId);
|
|
654
|
+
if (!track) {
|
|
655
|
+
throw new Error(`Track not found: ${clip.trackId}`);
|
|
656
|
+
}
|
|
657
|
+
const clipDuration = getClipDuration(clip);
|
|
658
|
+
const deleteEnd = clip.timelineEnd;
|
|
659
|
+
const clipsToShift = track.clips.filter(
|
|
660
|
+
(c) => c.id !== clipId && c.timelineStart >= deleteEnd
|
|
661
|
+
);
|
|
662
|
+
let tx = beginTransaction(state);
|
|
663
|
+
tx = applyOperation(tx, (s) => removeClip(s, clipId));
|
|
664
|
+
for (const clipToShift of clipsToShift) {
|
|
665
|
+
const newStart = clipToShift.timelineStart - clipDuration;
|
|
666
|
+
tx = applyOperation(tx, (s) => moveClip(s, clipToShift.id, newStart));
|
|
667
|
+
}
|
|
668
|
+
return commitTransaction(tx);
|
|
669
|
+
}
|
|
670
|
+
function rippleTrim(state, clipId, newEnd) {
|
|
671
|
+
const clip = findClipById(state, clipId);
|
|
672
|
+
if (!clip) {
|
|
673
|
+
throw new Error(`Clip not found: ${clipId}`);
|
|
674
|
+
}
|
|
675
|
+
const track = findTrackById(state, clip.trackId);
|
|
676
|
+
if (!track) {
|
|
677
|
+
throw new Error(`Track not found: ${clip.trackId}`);
|
|
678
|
+
}
|
|
679
|
+
if (newEnd <= clip.timelineStart) {
|
|
680
|
+
throw new Error("New end must be after clip start");
|
|
681
|
+
}
|
|
682
|
+
const delta = newEnd - clip.timelineEnd;
|
|
683
|
+
const clipsToShift = track.clips.filter(
|
|
684
|
+
(c) => c.id !== clipId && c.timelineStart >= clip.timelineEnd
|
|
685
|
+
);
|
|
686
|
+
let tx = beginTransaction(state);
|
|
687
|
+
tx = applyOperation(tx, (s) => resizeClip(s, clipId, clip.timelineStart, newEnd));
|
|
688
|
+
for (const clipToShift of clipsToShift) {
|
|
689
|
+
const newStart = clipToShift.timelineStart + delta;
|
|
690
|
+
tx = applyOperation(tx, (s) => moveClip(s, clipToShift.id, newStart));
|
|
691
|
+
}
|
|
692
|
+
return commitTransaction(tx);
|
|
693
|
+
}
|
|
694
|
+
function insertEdit(state, trackId, clip, atFrame) {
|
|
695
|
+
const track = findTrackById(state, trackId);
|
|
696
|
+
if (!track) {
|
|
697
|
+
throw new Error(`Track not found: ${trackId}`);
|
|
698
|
+
}
|
|
699
|
+
const clipDuration = getClipDuration(clip);
|
|
700
|
+
const clipsToShift = track.clips.filter((c) => c.timelineStart >= atFrame);
|
|
701
|
+
const adjustedClip = {
|
|
702
|
+
...clip,
|
|
703
|
+
timelineStart: atFrame,
|
|
704
|
+
timelineEnd: atFrame + clipDuration
|
|
705
|
+
};
|
|
706
|
+
let tx = beginTransaction(state);
|
|
707
|
+
for (const clipToShift of clipsToShift) {
|
|
708
|
+
const newStart = clipToShift.timelineStart + clipDuration;
|
|
709
|
+
tx = applyOperation(tx, (s) => moveClip(s, clipToShift.id, newStart));
|
|
710
|
+
}
|
|
711
|
+
tx = applyOperation(tx, (s) => addClip(s, trackId, adjustedClip));
|
|
712
|
+
return commitTransaction(tx);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// src/operations/marker-operations.ts
|
|
716
|
+
function addTimelineMarker(state, marker) {
|
|
717
|
+
return {
|
|
718
|
+
...state,
|
|
719
|
+
markers: {
|
|
720
|
+
...state.markers,
|
|
721
|
+
timeline: [...state.markers.timeline, marker]
|
|
722
|
+
}
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
function addClipMarker(state, marker) {
|
|
726
|
+
const clip = findClipById(state, marker.clipId);
|
|
727
|
+
if (!clip) {
|
|
728
|
+
throw new Error(`Clip not found: ${marker.clipId}`);
|
|
729
|
+
}
|
|
730
|
+
return {
|
|
731
|
+
...state,
|
|
732
|
+
markers: {
|
|
733
|
+
...state.markers,
|
|
734
|
+
clips: [...state.markers.clips, marker]
|
|
735
|
+
}
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
function addRegionMarker(state, marker) {
|
|
739
|
+
if (marker.startFrame >= marker.endFrame) {
|
|
740
|
+
throw new Error("Region marker start must be before end");
|
|
741
|
+
}
|
|
742
|
+
return {
|
|
743
|
+
...state,
|
|
744
|
+
markers: {
|
|
745
|
+
...state.markers,
|
|
746
|
+
regions: [...state.markers.regions, marker]
|
|
747
|
+
}
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
function removeMarker(state, markerId) {
|
|
751
|
+
return {
|
|
752
|
+
...state,
|
|
753
|
+
markers: {
|
|
754
|
+
timeline: state.markers.timeline.filter((m) => m.id !== markerId),
|
|
755
|
+
clips: state.markers.clips.filter((m) => m.id !== markerId),
|
|
756
|
+
regions: state.markers.regions.filter((m) => m.id !== markerId)
|
|
757
|
+
}
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
function removeClipMarkers(state, clipId) {
|
|
761
|
+
return {
|
|
762
|
+
...state,
|
|
763
|
+
markers: {
|
|
764
|
+
...state.markers,
|
|
765
|
+
clips: state.markers.clips.filter((m) => m.clipId !== clipId)
|
|
766
|
+
}
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
function setWorkArea(state, workArea) {
|
|
770
|
+
if (workArea.startFrame >= workArea.endFrame) {
|
|
771
|
+
throw new Error("Work area start must be before end");
|
|
772
|
+
}
|
|
773
|
+
return {
|
|
774
|
+
...state,
|
|
775
|
+
workArea
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
function clearWorkArea(state) {
|
|
779
|
+
const { workArea: _, ...stateWithoutWorkArea } = state;
|
|
780
|
+
return stateWithoutWorkArea;
|
|
781
|
+
}
|
|
782
|
+
function updateTimelineMarker(state, markerId, updates) {
|
|
783
|
+
return {
|
|
784
|
+
...state,
|
|
785
|
+
markers: {
|
|
786
|
+
...state.markers,
|
|
787
|
+
timeline: state.markers.timeline.map(
|
|
788
|
+
(m) => m.id === markerId ? { ...m, ...updates } : m
|
|
789
|
+
)
|
|
790
|
+
}
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
function updateRegionMarker(state, markerId, updates) {
|
|
794
|
+
return {
|
|
795
|
+
...state,
|
|
796
|
+
markers: {
|
|
797
|
+
...state.markers,
|
|
798
|
+
regions: state.markers.regions.map(
|
|
799
|
+
(m) => m.id === markerId ? { ...m, ...updates } : m
|
|
800
|
+
)
|
|
801
|
+
}
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// src/engine/history.ts
|
|
806
|
+
function createHistory(initialState, limit = 50) {
|
|
807
|
+
return {
|
|
808
|
+
past: [],
|
|
809
|
+
present: initialState,
|
|
810
|
+
future: [],
|
|
811
|
+
limit
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
function pushHistory(history, newState) {
|
|
815
|
+
const newPast = [...history.past, history.present];
|
|
816
|
+
if (newPast.length > history.limit) {
|
|
817
|
+
newPast.shift();
|
|
818
|
+
}
|
|
819
|
+
return {
|
|
820
|
+
...history,
|
|
821
|
+
past: newPast,
|
|
822
|
+
present: newState,
|
|
823
|
+
future: []
|
|
824
|
+
// Clear future on new action
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
function undo(history) {
|
|
828
|
+
if (history.past.length === 0) {
|
|
829
|
+
return history;
|
|
830
|
+
}
|
|
831
|
+
const newPast = [...history.past];
|
|
832
|
+
const previous = newPast.pop();
|
|
833
|
+
return {
|
|
834
|
+
...history,
|
|
835
|
+
past: newPast,
|
|
836
|
+
present: previous,
|
|
837
|
+
future: [history.present, ...history.future]
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
function redo(history) {
|
|
841
|
+
if (history.future.length === 0) {
|
|
842
|
+
return history;
|
|
843
|
+
}
|
|
844
|
+
const newFuture = [...history.future];
|
|
845
|
+
const next = newFuture.shift();
|
|
846
|
+
return {
|
|
847
|
+
...history,
|
|
848
|
+
past: [...history.past, history.present],
|
|
849
|
+
present: next,
|
|
850
|
+
future: newFuture
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
function canUndo(history) {
|
|
854
|
+
return history.past.length > 0;
|
|
855
|
+
}
|
|
856
|
+
function canRedo(history) {
|
|
857
|
+
return history.future.length > 0;
|
|
858
|
+
}
|
|
859
|
+
function getCurrentState(history) {
|
|
860
|
+
return history.present;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// src/engine/dispatcher.ts
|
|
864
|
+
function dispatch(history, operation) {
|
|
865
|
+
const currentState = getCurrentState(history);
|
|
866
|
+
let newState;
|
|
867
|
+
try {
|
|
868
|
+
newState = operation(currentState);
|
|
869
|
+
} catch (error) {
|
|
870
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
871
|
+
return {
|
|
872
|
+
success: false,
|
|
873
|
+
history,
|
|
874
|
+
// Return unchanged history
|
|
875
|
+
errors: [{
|
|
876
|
+
code: "OPERATION_ERROR",
|
|
877
|
+
message: errorMessage
|
|
878
|
+
}]
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
const validationResult = validateTimeline(newState);
|
|
882
|
+
if (!validationResult.valid) {
|
|
883
|
+
return {
|
|
884
|
+
success: false,
|
|
885
|
+
history,
|
|
886
|
+
// Return unchanged history
|
|
887
|
+
errors: validationResult.errors
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
const newHistory = pushHistory(history, newState);
|
|
891
|
+
return {
|
|
892
|
+
success: true,
|
|
893
|
+
history: newHistory
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
// src/engine/timeline-engine.ts
|
|
898
|
+
var TimelineEngine = class {
|
|
899
|
+
history;
|
|
900
|
+
listeners = /* @__PURE__ */ new Set();
|
|
901
|
+
/**
|
|
902
|
+
* Create a new timeline engine
|
|
903
|
+
*
|
|
904
|
+
* @param initialState - Initial timeline state
|
|
905
|
+
* @param historyLimit - Maximum number of undo steps (default: 50)
|
|
906
|
+
*/
|
|
907
|
+
constructor(initialState, historyLimit = 50) {
|
|
908
|
+
this.history = createHistory(initialState, historyLimit);
|
|
909
|
+
}
|
|
910
|
+
// ===== SUBSCRIPTION =====
|
|
911
|
+
/**
|
|
912
|
+
* Subscribe to state changes
|
|
913
|
+
*
|
|
914
|
+
* The listener will be called whenever the timeline state changes,
|
|
915
|
+
* with the new state passed as an argument.
|
|
916
|
+
* This is used by framework adapters (e.g., React) to trigger re-renders.
|
|
917
|
+
*
|
|
918
|
+
* @param listener - Function to call on state changes, receives new state
|
|
919
|
+
* @returns Unsubscribe function
|
|
920
|
+
*
|
|
921
|
+
* @example
|
|
922
|
+
* ```typescript
|
|
923
|
+
* const unsubscribe = engine.subscribe((state) => {
|
|
924
|
+
* console.log('State changed:', state);
|
|
925
|
+
* });
|
|
926
|
+
*
|
|
927
|
+
* // Later...
|
|
928
|
+
* unsubscribe();
|
|
929
|
+
* ```
|
|
930
|
+
*/
|
|
931
|
+
subscribe(listener) {
|
|
932
|
+
this.listeners.add(listener);
|
|
933
|
+
return () => {
|
|
934
|
+
this.listeners.delete(listener);
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
/**
|
|
938
|
+
* Notify all subscribers of a state change
|
|
939
|
+
*
|
|
940
|
+
* This is called internally after any operation that modifies state.
|
|
941
|
+
* Framework adapters use this to trigger re-renders.
|
|
942
|
+
*/
|
|
943
|
+
notify() {
|
|
944
|
+
const state = this.getState();
|
|
945
|
+
this.listeners.forEach((listener) => listener(state));
|
|
946
|
+
}
|
|
947
|
+
// ===== STATE ACCESS =====
|
|
948
|
+
/**
|
|
949
|
+
* Get the current timeline state
|
|
950
|
+
*
|
|
951
|
+
* @returns Current timeline state
|
|
952
|
+
*/
|
|
953
|
+
getState() {
|
|
954
|
+
return getCurrentState(this.history);
|
|
955
|
+
}
|
|
956
|
+
// ===== ASSET OPERATIONS =====
|
|
957
|
+
/**
|
|
958
|
+
* Register an asset
|
|
959
|
+
*
|
|
960
|
+
* @param asset - Asset to register
|
|
961
|
+
* @returns Dispatch result
|
|
962
|
+
*/
|
|
963
|
+
registerAsset(asset) {
|
|
964
|
+
const result = dispatch(
|
|
965
|
+
this.history,
|
|
966
|
+
(state) => registerAsset(state, asset)
|
|
967
|
+
);
|
|
968
|
+
if (result.success) {
|
|
969
|
+
this.history = result.history;
|
|
970
|
+
this.notify();
|
|
971
|
+
}
|
|
972
|
+
return result;
|
|
973
|
+
}
|
|
974
|
+
/**
|
|
975
|
+
* Get an asset by ID
|
|
976
|
+
*
|
|
977
|
+
* @param assetId - Asset ID
|
|
978
|
+
* @returns The asset, or undefined if not found
|
|
979
|
+
*/
|
|
980
|
+
getAsset(assetId) {
|
|
981
|
+
return getAsset(this.getState(), assetId);
|
|
982
|
+
}
|
|
983
|
+
// ===== CLIP OPERATIONS =====
|
|
984
|
+
/**
|
|
985
|
+
* Add a clip to a track
|
|
986
|
+
*
|
|
987
|
+
* @param trackId - ID of the track to add to
|
|
988
|
+
* @param clip - Clip to add
|
|
989
|
+
* @returns Dispatch result
|
|
990
|
+
*/
|
|
991
|
+
addClip(trackId, clip) {
|
|
992
|
+
const result = dispatch(
|
|
993
|
+
this.history,
|
|
994
|
+
(state) => addClip(state, trackId, clip)
|
|
995
|
+
);
|
|
996
|
+
if (result.success) {
|
|
997
|
+
this.history = result.history;
|
|
998
|
+
this.notify();
|
|
999
|
+
}
|
|
1000
|
+
return result;
|
|
1001
|
+
}
|
|
1002
|
+
/**
|
|
1003
|
+
* Remove a clip
|
|
1004
|
+
*
|
|
1005
|
+
* @param clipId - ID of the clip to remove
|
|
1006
|
+
* @returns Dispatch result
|
|
1007
|
+
*/
|
|
1008
|
+
removeClip(clipId) {
|
|
1009
|
+
const result = dispatch(
|
|
1010
|
+
this.history,
|
|
1011
|
+
(state) => removeClip(state, clipId)
|
|
1012
|
+
);
|
|
1013
|
+
if (result.success) {
|
|
1014
|
+
this.history = result.history;
|
|
1015
|
+
this.notify();
|
|
1016
|
+
}
|
|
1017
|
+
return result;
|
|
1018
|
+
}
|
|
1019
|
+
/**
|
|
1020
|
+
* Move a clip to a new timeline position
|
|
1021
|
+
*
|
|
1022
|
+
* @param clipId - ID of the clip to move
|
|
1023
|
+
* @param newStart - New timeline start frame
|
|
1024
|
+
* @returns Dispatch result
|
|
1025
|
+
*/
|
|
1026
|
+
moveClip(clipId, newStart) {
|
|
1027
|
+
const result = dispatch(
|
|
1028
|
+
this.history,
|
|
1029
|
+
(state) => moveClip(state, clipId, newStart)
|
|
1030
|
+
);
|
|
1031
|
+
if (result.success) {
|
|
1032
|
+
this.history = result.history;
|
|
1033
|
+
this.notify();
|
|
1034
|
+
}
|
|
1035
|
+
return result;
|
|
1036
|
+
}
|
|
1037
|
+
/**
|
|
1038
|
+
* Resize a clip
|
|
1039
|
+
*
|
|
1040
|
+
* @param clipId - ID of the clip to resize
|
|
1041
|
+
* @param newStart - New timeline start frame
|
|
1042
|
+
* @param newEnd - New timeline end frame
|
|
1043
|
+
* @returns Dispatch result
|
|
1044
|
+
*/
|
|
1045
|
+
resizeClip(clipId, newStart, newEnd) {
|
|
1046
|
+
const result = dispatch(
|
|
1047
|
+
this.history,
|
|
1048
|
+
(state) => resizeClip(state, clipId, newStart, newEnd)
|
|
1049
|
+
);
|
|
1050
|
+
if (result.success) {
|
|
1051
|
+
this.history = result.history;
|
|
1052
|
+
this.notify();
|
|
1053
|
+
}
|
|
1054
|
+
return result;
|
|
1055
|
+
}
|
|
1056
|
+
/**
|
|
1057
|
+
* Trim a clip (change media bounds)
|
|
1058
|
+
*
|
|
1059
|
+
* @param clipId - ID of the clip to trim
|
|
1060
|
+
* @param newMediaIn - New media in frame
|
|
1061
|
+
* @param newMediaOut - New media out frame
|
|
1062
|
+
* @returns Dispatch result
|
|
1063
|
+
*/
|
|
1064
|
+
trimClip(clipId, newMediaIn, newMediaOut) {
|
|
1065
|
+
const result = dispatch(
|
|
1066
|
+
this.history,
|
|
1067
|
+
(state) => trimClip(state, clipId, newMediaIn, newMediaOut)
|
|
1068
|
+
);
|
|
1069
|
+
if (result.success) {
|
|
1070
|
+
this.history = result.history;
|
|
1071
|
+
this.notify();
|
|
1072
|
+
}
|
|
1073
|
+
return result;
|
|
1074
|
+
}
|
|
1075
|
+
/**
|
|
1076
|
+
* Move a clip to a different track
|
|
1077
|
+
*
|
|
1078
|
+
* @param clipId - ID of the clip to move
|
|
1079
|
+
* @param targetTrackId - ID of the target track
|
|
1080
|
+
* @returns Dispatch result
|
|
1081
|
+
*/
|
|
1082
|
+
moveClipToTrack(clipId, targetTrackId) {
|
|
1083
|
+
const result = dispatch(
|
|
1084
|
+
this.history,
|
|
1085
|
+
(state) => moveClipToTrack(state, clipId, targetTrackId)
|
|
1086
|
+
);
|
|
1087
|
+
if (result.success) {
|
|
1088
|
+
this.history = result.history;
|
|
1089
|
+
this.notify();
|
|
1090
|
+
}
|
|
1091
|
+
return result;
|
|
1092
|
+
}
|
|
1093
|
+
// ===== TRACK OPERATIONS =====
|
|
1094
|
+
/**
|
|
1095
|
+
* Add a track
|
|
1096
|
+
*
|
|
1097
|
+
* @param track - Track to add
|
|
1098
|
+
* @returns Dispatch result
|
|
1099
|
+
*/
|
|
1100
|
+
addTrack(track) {
|
|
1101
|
+
const result = dispatch(
|
|
1102
|
+
this.history,
|
|
1103
|
+
(state) => addTrack(state, track)
|
|
1104
|
+
);
|
|
1105
|
+
if (result.success) {
|
|
1106
|
+
this.history = result.history;
|
|
1107
|
+
this.notify();
|
|
1108
|
+
}
|
|
1109
|
+
return result;
|
|
1110
|
+
}
|
|
1111
|
+
/**
|
|
1112
|
+
* Remove a track
|
|
1113
|
+
*
|
|
1114
|
+
* @param trackId - ID of the track to remove
|
|
1115
|
+
* @returns Dispatch result
|
|
1116
|
+
*/
|
|
1117
|
+
removeTrack(trackId) {
|
|
1118
|
+
const result = dispatch(
|
|
1119
|
+
this.history,
|
|
1120
|
+
(state) => removeTrack(state, trackId)
|
|
1121
|
+
);
|
|
1122
|
+
if (result.success) {
|
|
1123
|
+
this.history = result.history;
|
|
1124
|
+
this.notify();
|
|
1125
|
+
}
|
|
1126
|
+
return result;
|
|
1127
|
+
}
|
|
1128
|
+
/**
|
|
1129
|
+
* Move a track to a new position
|
|
1130
|
+
*
|
|
1131
|
+
* @param trackId - ID of the track to move
|
|
1132
|
+
* @param newIndex - New index position
|
|
1133
|
+
* @returns Dispatch result
|
|
1134
|
+
*/
|
|
1135
|
+
moveTrack(trackId, newIndex) {
|
|
1136
|
+
const result = dispatch(
|
|
1137
|
+
this.history,
|
|
1138
|
+
(state) => moveTrack(state, trackId, newIndex)
|
|
1139
|
+
);
|
|
1140
|
+
if (result.success) {
|
|
1141
|
+
this.history = result.history;
|
|
1142
|
+
this.notify();
|
|
1143
|
+
}
|
|
1144
|
+
return result;
|
|
1145
|
+
}
|
|
1146
|
+
/**
|
|
1147
|
+
* Toggle track mute
|
|
1148
|
+
*
|
|
1149
|
+
* @param trackId - ID of the track
|
|
1150
|
+
* @returns Dispatch result
|
|
1151
|
+
*/
|
|
1152
|
+
toggleTrackMute(trackId) {
|
|
1153
|
+
const result = dispatch(
|
|
1154
|
+
this.history,
|
|
1155
|
+
(state) => toggleTrackMute(state, trackId)
|
|
1156
|
+
);
|
|
1157
|
+
if (result.success) {
|
|
1158
|
+
this.history = result.history;
|
|
1159
|
+
this.notify();
|
|
1160
|
+
}
|
|
1161
|
+
return result;
|
|
1162
|
+
}
|
|
1163
|
+
/**
|
|
1164
|
+
* Toggle track lock
|
|
1165
|
+
*
|
|
1166
|
+
* @param trackId - ID of the track
|
|
1167
|
+
* @returns Dispatch result
|
|
1168
|
+
*/
|
|
1169
|
+
toggleTrackLock(trackId) {
|
|
1170
|
+
const result = dispatch(
|
|
1171
|
+
this.history,
|
|
1172
|
+
(state) => toggleTrackLock(state, trackId)
|
|
1173
|
+
);
|
|
1174
|
+
if (result.success) {
|
|
1175
|
+
this.history = result.history;
|
|
1176
|
+
this.notify();
|
|
1177
|
+
}
|
|
1178
|
+
return result;
|
|
1179
|
+
}
|
|
1180
|
+
/**
|
|
1181
|
+
* Toggle track solo
|
|
1182
|
+
*
|
|
1183
|
+
* @param trackId - ID of the track
|
|
1184
|
+
* @returns Dispatch result
|
|
1185
|
+
*/
|
|
1186
|
+
toggleTrackSolo(trackId) {
|
|
1187
|
+
const result = dispatch(
|
|
1188
|
+
this.history,
|
|
1189
|
+
(state) => toggleTrackSolo(state, trackId)
|
|
1190
|
+
);
|
|
1191
|
+
if (result.success) {
|
|
1192
|
+
this.history = result.history;
|
|
1193
|
+
this.notify();
|
|
1194
|
+
}
|
|
1195
|
+
return result;
|
|
1196
|
+
}
|
|
1197
|
+
/**
|
|
1198
|
+
* Set track height
|
|
1199
|
+
*
|
|
1200
|
+
* @param trackId - ID of the track
|
|
1201
|
+
* @param height - New height in pixels
|
|
1202
|
+
* @returns Dispatch result
|
|
1203
|
+
*/
|
|
1204
|
+
setTrackHeight(trackId, height) {
|
|
1205
|
+
const result = dispatch(
|
|
1206
|
+
this.history,
|
|
1207
|
+
(state) => setTrackHeight(state, trackId, height)
|
|
1208
|
+
);
|
|
1209
|
+
if (result.success) {
|
|
1210
|
+
this.history = result.history;
|
|
1211
|
+
this.notify();
|
|
1212
|
+
}
|
|
1213
|
+
return result;
|
|
1214
|
+
}
|
|
1215
|
+
// ===== TIMELINE OPERATIONS =====
|
|
1216
|
+
/**
|
|
1217
|
+
* Set timeline duration
|
|
1218
|
+
*
|
|
1219
|
+
* @param duration - New duration in frames
|
|
1220
|
+
* @returns Dispatch result
|
|
1221
|
+
*/
|
|
1222
|
+
setTimelineDuration(duration) {
|
|
1223
|
+
const result = dispatch(
|
|
1224
|
+
this.history,
|
|
1225
|
+
(state) => setTimelineDuration(state, duration)
|
|
1226
|
+
);
|
|
1227
|
+
if (result.success) {
|
|
1228
|
+
this.history = result.history;
|
|
1229
|
+
this.notify();
|
|
1230
|
+
}
|
|
1231
|
+
return result;
|
|
1232
|
+
}
|
|
1233
|
+
/**
|
|
1234
|
+
* Set timeline name
|
|
1235
|
+
*
|
|
1236
|
+
* @param name - New timeline name
|
|
1237
|
+
* @returns Dispatch result
|
|
1238
|
+
*/
|
|
1239
|
+
setTimelineName(name) {
|
|
1240
|
+
const result = dispatch(
|
|
1241
|
+
this.history,
|
|
1242
|
+
(state) => setTimelineName(state, name)
|
|
1243
|
+
);
|
|
1244
|
+
if (result.success) {
|
|
1245
|
+
this.history = result.history;
|
|
1246
|
+
this.notify();
|
|
1247
|
+
}
|
|
1248
|
+
return result;
|
|
1249
|
+
}
|
|
1250
|
+
// ===== HISTORY OPERATIONS =====
|
|
1251
|
+
/**
|
|
1252
|
+
* Undo the last action
|
|
1253
|
+
*
|
|
1254
|
+
* @returns true if undo was performed
|
|
1255
|
+
*/
|
|
1256
|
+
undo() {
|
|
1257
|
+
if (!this.canUndo()) {
|
|
1258
|
+
return false;
|
|
1259
|
+
}
|
|
1260
|
+
this.history = undo(this.history);
|
|
1261
|
+
this.notify();
|
|
1262
|
+
return true;
|
|
1263
|
+
}
|
|
1264
|
+
/**
|
|
1265
|
+
* Redo the last undone action
|
|
1266
|
+
*
|
|
1267
|
+
* @returns true if redo was performed
|
|
1268
|
+
*/
|
|
1269
|
+
redo() {
|
|
1270
|
+
if (!this.canRedo()) {
|
|
1271
|
+
return false;
|
|
1272
|
+
}
|
|
1273
|
+
this.history = redo(this.history);
|
|
1274
|
+
this.notify();
|
|
1275
|
+
return true;
|
|
1276
|
+
}
|
|
1277
|
+
/**
|
|
1278
|
+
* Check if undo is available
|
|
1279
|
+
*
|
|
1280
|
+
* @returns true if undo is available
|
|
1281
|
+
*/
|
|
1282
|
+
canUndo() {
|
|
1283
|
+
return canUndo(this.history);
|
|
1284
|
+
}
|
|
1285
|
+
/**
|
|
1286
|
+
* Check if redo is available
|
|
1287
|
+
*
|
|
1288
|
+
* @returns true if redo is available
|
|
1289
|
+
*/
|
|
1290
|
+
canRedo() {
|
|
1291
|
+
return canRedo(this.history);
|
|
1292
|
+
}
|
|
1293
|
+
// ===== QUERY OPERATIONS =====
|
|
1294
|
+
/**
|
|
1295
|
+
* Find a clip by ID
|
|
1296
|
+
*
|
|
1297
|
+
* @param clipId - Clip ID
|
|
1298
|
+
* @returns The clip, or undefined if not found
|
|
1299
|
+
*/
|
|
1300
|
+
findClipById(clipId) {
|
|
1301
|
+
return findClipById(this.getState(), clipId);
|
|
1302
|
+
}
|
|
1303
|
+
/**
|
|
1304
|
+
* Find a track by ID
|
|
1305
|
+
*
|
|
1306
|
+
* @param trackId - Track ID
|
|
1307
|
+
* @returns The track, or undefined if not found
|
|
1308
|
+
*/
|
|
1309
|
+
findTrackById(trackId) {
|
|
1310
|
+
return findTrackById(this.getState(), trackId);
|
|
1311
|
+
}
|
|
1312
|
+
/**
|
|
1313
|
+
* Get all clips on a track
|
|
1314
|
+
*
|
|
1315
|
+
* @param trackId - Track ID
|
|
1316
|
+
* @returns Array of clips on the track
|
|
1317
|
+
*/
|
|
1318
|
+
getClipsOnTrack(trackId) {
|
|
1319
|
+
return getClipsOnTrack(this.getState(), trackId);
|
|
1320
|
+
}
|
|
1321
|
+
/**
|
|
1322
|
+
* Get all clips at a specific frame
|
|
1323
|
+
*
|
|
1324
|
+
* @param frame - Frame to check
|
|
1325
|
+
* @returns Array of clips at that frame
|
|
1326
|
+
*/
|
|
1327
|
+
getClipsAtFrame(frame2) {
|
|
1328
|
+
return getClipsAtFrame(this.getState(), frame2);
|
|
1329
|
+
}
|
|
1330
|
+
/**
|
|
1331
|
+
* Get all clips in a frame range
|
|
1332
|
+
*
|
|
1333
|
+
* @param start - Start frame
|
|
1334
|
+
* @param end - End frame
|
|
1335
|
+
* @returns Array of clips in the range
|
|
1336
|
+
*/
|
|
1337
|
+
getClipsInRange(start, end) {
|
|
1338
|
+
return getClipsInRange(this.getState(), start, end);
|
|
1339
|
+
}
|
|
1340
|
+
/**
|
|
1341
|
+
* Get all clips in the timeline
|
|
1342
|
+
*
|
|
1343
|
+
* @returns Array of all clips
|
|
1344
|
+
*/
|
|
1345
|
+
getAllClips() {
|
|
1346
|
+
return getAllClips(this.getState());
|
|
1347
|
+
}
|
|
1348
|
+
/**
|
|
1349
|
+
* Get all tracks in the timeline
|
|
1350
|
+
*
|
|
1351
|
+
* @returns Array of all tracks
|
|
1352
|
+
*/
|
|
1353
|
+
getAllTracks() {
|
|
1354
|
+
return getAllTracks(this.getState());
|
|
1355
|
+
}
|
|
1356
|
+
// ===== RIPPLE OPERATIONS =====
|
|
1357
|
+
/**
|
|
1358
|
+
* Ripple delete - delete clip and shift subsequent clips left
|
|
1359
|
+
*
|
|
1360
|
+
* @param clipId - ID of the clip to delete
|
|
1361
|
+
* @returns Dispatch result
|
|
1362
|
+
*/
|
|
1363
|
+
rippleDelete(clipId) {
|
|
1364
|
+
const result = dispatch(
|
|
1365
|
+
this.history,
|
|
1366
|
+
(state) => rippleDelete(state, clipId)
|
|
1367
|
+
);
|
|
1368
|
+
if (result.success) {
|
|
1369
|
+
this.history = result.history;
|
|
1370
|
+
this.notify();
|
|
1371
|
+
}
|
|
1372
|
+
return result;
|
|
1373
|
+
}
|
|
1374
|
+
/**
|
|
1375
|
+
* Ripple trim - trim clip end and shift subsequent clips
|
|
1376
|
+
*
|
|
1377
|
+
* @param clipId - ID of the clip to trim
|
|
1378
|
+
* @param newEnd - New end frame for the clip
|
|
1379
|
+
* @returns Dispatch result
|
|
1380
|
+
*/
|
|
1381
|
+
rippleTrim(clipId, newEnd) {
|
|
1382
|
+
const result = dispatch(
|
|
1383
|
+
this.history,
|
|
1384
|
+
(state) => rippleTrim(state, clipId, newEnd)
|
|
1385
|
+
);
|
|
1386
|
+
if (result.success) {
|
|
1387
|
+
this.history = result.history;
|
|
1388
|
+
this.notify();
|
|
1389
|
+
}
|
|
1390
|
+
return result;
|
|
1391
|
+
}
|
|
1392
|
+
/**
|
|
1393
|
+
* Insert edit - insert clip and shift subsequent clips right
|
|
1394
|
+
*
|
|
1395
|
+
* @param trackId - ID of the track to insert into
|
|
1396
|
+
* @param clip - Clip to insert
|
|
1397
|
+
* @param atFrame - Frame to insert at
|
|
1398
|
+
* @returns Dispatch result
|
|
1399
|
+
*/
|
|
1400
|
+
insertEdit(trackId, clip, atFrame) {
|
|
1401
|
+
const result = dispatch(
|
|
1402
|
+
this.history,
|
|
1403
|
+
(state) => insertEdit(state, trackId, clip, atFrame)
|
|
1404
|
+
);
|
|
1405
|
+
if (result.success) {
|
|
1406
|
+
this.history = result.history;
|
|
1407
|
+
this.notify();
|
|
1408
|
+
}
|
|
1409
|
+
return result;
|
|
1410
|
+
}
|
|
1411
|
+
// ===== MARKER OPERATIONS =====
|
|
1412
|
+
/**
|
|
1413
|
+
* Add a timeline marker
|
|
1414
|
+
*
|
|
1415
|
+
* @param marker - Timeline marker to add
|
|
1416
|
+
* @returns Dispatch result
|
|
1417
|
+
*/
|
|
1418
|
+
addTimelineMarker(marker) {
|
|
1419
|
+
const result = dispatch(
|
|
1420
|
+
this.history,
|
|
1421
|
+
(state) => addTimelineMarker(state, marker)
|
|
1422
|
+
);
|
|
1423
|
+
if (result.success) {
|
|
1424
|
+
this.history = result.history;
|
|
1425
|
+
this.notify();
|
|
1426
|
+
}
|
|
1427
|
+
return result;
|
|
1428
|
+
}
|
|
1429
|
+
/**
|
|
1430
|
+
* Add a clip marker
|
|
1431
|
+
*
|
|
1432
|
+
* @param marker - Clip marker to add
|
|
1433
|
+
* @returns Dispatch result
|
|
1434
|
+
*/
|
|
1435
|
+
addClipMarker(marker) {
|
|
1436
|
+
const result = dispatch(
|
|
1437
|
+
this.history,
|
|
1438
|
+
(state) => addClipMarker(state, marker)
|
|
1439
|
+
);
|
|
1440
|
+
if (result.success) {
|
|
1441
|
+
this.history = result.history;
|
|
1442
|
+
this.notify();
|
|
1443
|
+
}
|
|
1444
|
+
return result;
|
|
1445
|
+
}
|
|
1446
|
+
/**
|
|
1447
|
+
* Add a region marker
|
|
1448
|
+
*
|
|
1449
|
+
* @param marker - Region marker to add
|
|
1450
|
+
* @returns Dispatch result
|
|
1451
|
+
*/
|
|
1452
|
+
addRegionMarker(marker) {
|
|
1453
|
+
const result = dispatch(
|
|
1454
|
+
this.history,
|
|
1455
|
+
(state) => addRegionMarker(state, marker)
|
|
1456
|
+
);
|
|
1457
|
+
if (result.success) {
|
|
1458
|
+
this.history = result.history;
|
|
1459
|
+
this.notify();
|
|
1460
|
+
}
|
|
1461
|
+
return result;
|
|
1462
|
+
}
|
|
1463
|
+
/**
|
|
1464
|
+
* Remove a marker by ID
|
|
1465
|
+
*
|
|
1466
|
+
* @param markerId - ID of the marker to remove
|
|
1467
|
+
* @returns Dispatch result
|
|
1468
|
+
*/
|
|
1469
|
+
removeMarker(markerId) {
|
|
1470
|
+
const result = dispatch(
|
|
1471
|
+
this.history,
|
|
1472
|
+
(state) => removeMarker(state, markerId)
|
|
1473
|
+
);
|
|
1474
|
+
if (result.success) {
|
|
1475
|
+
this.history = result.history;
|
|
1476
|
+
this.notify();
|
|
1477
|
+
}
|
|
1478
|
+
return result;
|
|
1479
|
+
}
|
|
1480
|
+
/**
|
|
1481
|
+
* Update a timeline marker
|
|
1482
|
+
*
|
|
1483
|
+
* @param markerId - ID of the marker to update
|
|
1484
|
+
* @param updates - Partial marker updates
|
|
1485
|
+
* @returns Dispatch result
|
|
1486
|
+
*/
|
|
1487
|
+
updateTimelineMarker(markerId, updates) {
|
|
1488
|
+
const result = dispatch(
|
|
1489
|
+
this.history,
|
|
1490
|
+
(state) => updateTimelineMarker(state, markerId, updates)
|
|
1491
|
+
);
|
|
1492
|
+
if (result.success) {
|
|
1493
|
+
this.history = result.history;
|
|
1494
|
+
this.notify();
|
|
1495
|
+
}
|
|
1496
|
+
return result;
|
|
1497
|
+
}
|
|
1498
|
+
/**
|
|
1499
|
+
* Update a region marker
|
|
1500
|
+
*
|
|
1501
|
+
* @param markerId - ID of the marker to update
|
|
1502
|
+
* @param updates - Partial marker updates
|
|
1503
|
+
* @returns Dispatch result
|
|
1504
|
+
*/
|
|
1505
|
+
updateRegionMarker(markerId, updates) {
|
|
1506
|
+
const result = dispatch(
|
|
1507
|
+
this.history,
|
|
1508
|
+
(state) => updateRegionMarker(state, markerId, updates)
|
|
1509
|
+
);
|
|
1510
|
+
if (result.success) {
|
|
1511
|
+
this.history = result.history;
|
|
1512
|
+
this.notify();
|
|
1513
|
+
}
|
|
1514
|
+
return result;
|
|
1515
|
+
}
|
|
1516
|
+
// ===== WORK AREA OPERATIONS =====
|
|
1517
|
+
/**
|
|
1518
|
+
* Set work area
|
|
1519
|
+
*
|
|
1520
|
+
* @param start - Start frame
|
|
1521
|
+
* @param end - End frame
|
|
1522
|
+
* @returns Dispatch result
|
|
1523
|
+
*/
|
|
1524
|
+
setWorkArea(start, end) {
|
|
1525
|
+
const result = dispatch(
|
|
1526
|
+
this.history,
|
|
1527
|
+
(state) => setWorkArea(state, { startFrame: start, endFrame: end })
|
|
1528
|
+
);
|
|
1529
|
+
if (result.success) {
|
|
1530
|
+
this.history = result.history;
|
|
1531
|
+
this.notify();
|
|
1532
|
+
}
|
|
1533
|
+
return result;
|
|
1534
|
+
}
|
|
1535
|
+
/**
|
|
1536
|
+
* Clear work area
|
|
1537
|
+
*
|
|
1538
|
+
* @returns Dispatch result
|
|
1539
|
+
*/
|
|
1540
|
+
clearWorkArea() {
|
|
1541
|
+
const result = dispatch(
|
|
1542
|
+
this.history,
|
|
1543
|
+
(state) => clearWorkArea(state)
|
|
1544
|
+
);
|
|
1545
|
+
if (result.success) {
|
|
1546
|
+
this.history = result.history;
|
|
1547
|
+
this.notify();
|
|
1548
|
+
}
|
|
1549
|
+
return result;
|
|
1550
|
+
}
|
|
1551
|
+
};
|
|
1552
|
+
|
|
1553
|
+
// src/types/timeline.ts
|
|
1554
|
+
function createTimeline(params) {
|
|
1555
|
+
const timeline = {
|
|
1556
|
+
id: params.id,
|
|
1557
|
+
name: params.name,
|
|
1558
|
+
fps: params.fps,
|
|
1559
|
+
duration: params.duration,
|
|
1560
|
+
tracks: params.tracks ?? []
|
|
1561
|
+
};
|
|
1562
|
+
if (params.metadata !== void 0) {
|
|
1563
|
+
timeline.metadata = params.metadata;
|
|
1564
|
+
}
|
|
1565
|
+
return timeline;
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
// src/types/asset.ts
|
|
1569
|
+
function createAsset(params) {
|
|
1570
|
+
const asset = {
|
|
1571
|
+
id: params.id,
|
|
1572
|
+
type: params.type,
|
|
1573
|
+
duration: params.duration,
|
|
1574
|
+
sourceUrl: params.sourceUrl
|
|
1575
|
+
};
|
|
1576
|
+
if (params.metadata !== void 0) {
|
|
1577
|
+
asset.metadata = params.metadata;
|
|
1578
|
+
}
|
|
1579
|
+
return asset;
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
// src/types/state.ts
|
|
1583
|
+
function createTimelineState(params) {
|
|
1584
|
+
const state = {
|
|
1585
|
+
timeline: params.timeline,
|
|
1586
|
+
assets: params.assets ?? /* @__PURE__ */ new Map(),
|
|
1587
|
+
linkGroups: params.linkGroups ?? /* @__PURE__ */ new Map(),
|
|
1588
|
+
groups: params.groups ?? /* @__PURE__ */ new Map(),
|
|
1589
|
+
markers: params.markers ?? {
|
|
1590
|
+
timeline: [],
|
|
1591
|
+
clips: [],
|
|
1592
|
+
regions: []
|
|
1593
|
+
}
|
|
1594
|
+
};
|
|
1595
|
+
if (params.workArea !== void 0) {
|
|
1596
|
+
state.workArea = params.workArea;
|
|
1597
|
+
}
|
|
1598
|
+
return state;
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
// src/types/frame.ts
|
|
1602
|
+
function frame(value) {
|
|
1603
|
+
const rounded = Math.round(value);
|
|
1604
|
+
if (rounded < 0) {
|
|
1605
|
+
throw new Error(`Frame value must be non-negative, got: ${value}`);
|
|
1606
|
+
}
|
|
1607
|
+
return rounded;
|
|
1608
|
+
}
|
|
1609
|
+
function frameRate(value) {
|
|
1610
|
+
if (value <= 0) {
|
|
1611
|
+
throw new Error(`FrameRate must be positive, got: ${value}`);
|
|
1612
|
+
}
|
|
1613
|
+
return value;
|
|
1614
|
+
}
|
|
1615
|
+
function isValidFrame(value) {
|
|
1616
|
+
return Number.isInteger(value) && value >= 0;
|
|
1617
|
+
}
|
|
1618
|
+
function isValidFrameRate(value) {
|
|
1619
|
+
return value > 0;
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
// src/utils/frame.ts
|
|
1623
|
+
function framesToSeconds(frames, fps) {
|
|
1624
|
+
return frames / fps;
|
|
1625
|
+
}
|
|
1626
|
+
function secondsToFrames(seconds, fps) {
|
|
1627
|
+
return frame(seconds * fps);
|
|
1628
|
+
}
|
|
1629
|
+
function framesToTimecode(frames, fps) {
|
|
1630
|
+
const totalFrames = frames;
|
|
1631
|
+
const framesPart = totalFrames % fps;
|
|
1632
|
+
const totalSeconds = Math.floor(totalFrames / fps);
|
|
1633
|
+
const secondsPart = totalSeconds % 60;
|
|
1634
|
+
const totalMinutes = Math.floor(totalSeconds / 60);
|
|
1635
|
+
const minutesPart = totalMinutes % 60;
|
|
1636
|
+
const hoursPart = Math.floor(totalMinutes / 60);
|
|
1637
|
+
return `${pad(hoursPart)}:${pad(minutesPart)}:${pad(secondsPart)}:${pad(framesPart)}`;
|
|
1638
|
+
}
|
|
1639
|
+
function framesToMinutesSeconds(frames, fps) {
|
|
1640
|
+
const totalSeconds = Math.floor(frames / fps);
|
|
1641
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
1642
|
+
const seconds = totalSeconds % 60;
|
|
1643
|
+
return `${minutes}:${pad(seconds)}`;
|
|
1644
|
+
}
|
|
1645
|
+
function clampFrame(value, min, max) {
|
|
1646
|
+
return frame(Math.max(min, Math.min(max, value)));
|
|
1647
|
+
}
|
|
1648
|
+
function addFrames(a, b) {
|
|
1649
|
+
return frame(a + b);
|
|
1650
|
+
}
|
|
1651
|
+
function subtractFrames(a, b) {
|
|
1652
|
+
return frame(Math.max(0, a - b));
|
|
1653
|
+
}
|
|
1654
|
+
function frameDuration(start, end) {
|
|
1655
|
+
return frame(end - start);
|
|
1656
|
+
}
|
|
1657
|
+
function pad(num, width = 2) {
|
|
1658
|
+
return num.toString().padStart(width, "0");
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
export {
|
|
1662
|
+
createClip,
|
|
1663
|
+
getClipDuration,
|
|
1664
|
+
getClipMediaDuration,
|
|
1665
|
+
clipContainsFrame,
|
|
1666
|
+
clipsOverlap,
|
|
1667
|
+
validResult,
|
|
1668
|
+
invalidResult,
|
|
1669
|
+
invalidResults,
|
|
1670
|
+
combineResults,
|
|
1671
|
+
registerAsset,
|
|
1672
|
+
getAsset,
|
|
1673
|
+
hasAsset,
|
|
1674
|
+
getAllAssets,
|
|
1675
|
+
unregisterAsset,
|
|
1676
|
+
validateClip,
|
|
1677
|
+
validateTrack,
|
|
1678
|
+
validateTimeline,
|
|
1679
|
+
validateNoOverlap,
|
|
1680
|
+
findClipById,
|
|
1681
|
+
findTrackById,
|
|
1682
|
+
getClipsOnTrack,
|
|
1683
|
+
getClipsAtFrame,
|
|
1684
|
+
getClipsInRange,
|
|
1685
|
+
getAllClips,
|
|
1686
|
+
getAllTracks,
|
|
1687
|
+
findTrackIndex,
|
|
1688
|
+
createTrack,
|
|
1689
|
+
sortTrackClips,
|
|
1690
|
+
addClip,
|
|
1691
|
+
removeClip,
|
|
1692
|
+
moveClip,
|
|
1693
|
+
resizeClip,
|
|
1694
|
+
trimClip,
|
|
1695
|
+
updateClip,
|
|
1696
|
+
moveClipToTrack,
|
|
1697
|
+
addTrack,
|
|
1698
|
+
removeTrack,
|
|
1699
|
+
moveTrack,
|
|
1700
|
+
updateTrack,
|
|
1701
|
+
toggleTrackMute,
|
|
1702
|
+
toggleTrackLock,
|
|
1703
|
+
setTimelineDuration,
|
|
1704
|
+
setTimelineName,
|
|
1705
|
+
updateTimelineMetadata,
|
|
1706
|
+
beginTransaction,
|
|
1707
|
+
applyOperation,
|
|
1708
|
+
commitTransaction,
|
|
1709
|
+
rollbackTransaction,
|
|
1710
|
+
getOperationCount,
|
|
1711
|
+
rippleDelete,
|
|
1712
|
+
rippleTrim,
|
|
1713
|
+
insertEdit,
|
|
1714
|
+
addTimelineMarker,
|
|
1715
|
+
addClipMarker,
|
|
1716
|
+
addRegionMarker,
|
|
1717
|
+
removeMarker,
|
|
1718
|
+
removeClipMarkers,
|
|
1719
|
+
setWorkArea,
|
|
1720
|
+
clearWorkArea,
|
|
1721
|
+
updateTimelineMarker,
|
|
1722
|
+
updateRegionMarker,
|
|
1723
|
+
TimelineEngine,
|
|
1724
|
+
createTimeline,
|
|
1725
|
+
createAsset,
|
|
1726
|
+
createTimelineState,
|
|
1727
|
+
frame,
|
|
1728
|
+
frameRate,
|
|
1729
|
+
isValidFrame,
|
|
1730
|
+
isValidFrameRate,
|
|
1731
|
+
framesToSeconds,
|
|
1732
|
+
secondsToFrames,
|
|
1733
|
+
framesToTimecode,
|
|
1734
|
+
framesToMinutesSeconds,
|
|
1735
|
+
clampFrame,
|
|
1736
|
+
addFrames,
|
|
1737
|
+
subtractFrames,
|
|
1738
|
+
frameDuration
|
|
1739
|
+
};
|