@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,3255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FRAME-BASED TIME REPRESENTATION
|
|
3
|
+
*
|
|
4
|
+
* Phase 0 compliant. All time values in state are TimelineFrame branded integers.
|
|
5
|
+
* FrameRate is a discriminated literal union — never a raw float.
|
|
6
|
+
*
|
|
7
|
+
* THREE INVIOLABLE RULES:
|
|
8
|
+
* 1. Core has ZERO UI framework imports.
|
|
9
|
+
* 2. Every function that changes state returns a NEW object.
|
|
10
|
+
* 3. Every frame value is a branded TimelineFrame integer — never a raw number.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* TimelineFrame — A discrete, non-negative integer point in time measured in frames.
|
|
14
|
+
*
|
|
15
|
+
* Branded so TypeScript prevents raw numbers from sneaking into frame positions.
|
|
16
|
+
* The ONLY way to create one is via toFrame().
|
|
17
|
+
*/
|
|
18
|
+
type TimelineFrame = number & {
|
|
19
|
+
readonly __brand: "TimelineFrame";
|
|
20
|
+
};
|
|
21
|
+
/** The canonical factory. Use this everywhere instead of casting. */
|
|
22
|
+
declare const toFrame: (n: number) => TimelineFrame;
|
|
23
|
+
/**
|
|
24
|
+
* Legacy alias kept for backward-compat during transition.
|
|
25
|
+
* Prefer toFrame() for new code.
|
|
26
|
+
*/
|
|
27
|
+
declare function frame(value: number): TimelineFrame;
|
|
28
|
+
/**
|
|
29
|
+
* FrameRate — The exact set of supported frame rates.
|
|
30
|
+
*
|
|
31
|
+
* RULE: Never pass 29.97 as a plain number. Use the literal type.
|
|
32
|
+
* This is a discriminated union — TypeScript enforces membership at compile time.
|
|
33
|
+
*/
|
|
34
|
+
type FrameRate = 23.976 | 24 | 25 | 29.97 | 30 | 50 | 59.94 | 60;
|
|
35
|
+
/**
|
|
36
|
+
* Named constants for the most common rates.
|
|
37
|
+
* Prefer these over raw literals where possible.
|
|
38
|
+
*/
|
|
39
|
+
declare const FrameRates: {
|
|
40
|
+
readonly CINEMA: FrameRate;
|
|
41
|
+
readonly PAL: FrameRate;
|
|
42
|
+
readonly NTSC_DF: FrameRate;
|
|
43
|
+
readonly NTSC: FrameRate;
|
|
44
|
+
readonly PAL_HFR: FrameRate;
|
|
45
|
+
readonly NTSC_HFR: FrameRate;
|
|
46
|
+
readonly HFR: FrameRate;
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Legacy factory — kept for backward-compat with existing tests.
|
|
50
|
+
* This now validates that the value is a member of the FrameRate union.
|
|
51
|
+
* @throws if the value is not a recognised frame rate.
|
|
52
|
+
*/
|
|
53
|
+
declare function frameRate(value: number): FrameRate;
|
|
54
|
+
/**
|
|
55
|
+
* RationalTime — a frame count at a specific rate. Used only at
|
|
56
|
+
* ingest/export boundaries. Never stored in TimelineState.
|
|
57
|
+
*/
|
|
58
|
+
type RationalTime = {
|
|
59
|
+
readonly value: number;
|
|
60
|
+
readonly rate: FrameRate;
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* Timecode — SMPTE timecode string, display-only. Never use for arithmetic.
|
|
64
|
+
*/
|
|
65
|
+
type Timecode = string & {
|
|
66
|
+
readonly __brand: "Timecode";
|
|
67
|
+
};
|
|
68
|
+
declare const toTimecode: (s: string) => Timecode;
|
|
69
|
+
/**
|
|
70
|
+
* TimeRange — a start + duration pair, both in TimelineFrame units.
|
|
71
|
+
*/
|
|
72
|
+
type TimeRange = {
|
|
73
|
+
readonly startFrame: TimelineFrame;
|
|
74
|
+
readonly duration: TimelineFrame;
|
|
75
|
+
};
|
|
76
|
+
declare function isValidFrame(value: number): boolean;
|
|
77
|
+
declare function isDropFrame(fps: FrameRate): boolean;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* GENERATOR MODEL — Phase 3
|
|
81
|
+
*
|
|
82
|
+
* Generators are synthetic "assets" (solid, bars, countdown, etc.)
|
|
83
|
+
* registered in AssetRegistry as GeneratorAsset. No filePath.
|
|
84
|
+
*/
|
|
85
|
+
|
|
86
|
+
type GeneratorId = string & {
|
|
87
|
+
readonly __brand: 'GeneratorId';
|
|
88
|
+
};
|
|
89
|
+
type GeneratorType = 'solid' | 'bars' | 'countdown' | 'noise' | 'text';
|
|
90
|
+
type Generator = {
|
|
91
|
+
readonly id: GeneratorId;
|
|
92
|
+
readonly type: GeneratorType;
|
|
93
|
+
readonly params: Record<string, unknown>;
|
|
94
|
+
readonly duration: TimelineFrame;
|
|
95
|
+
readonly name: string;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* ASSET MODEL — Phase 0 + Phase 3
|
|
100
|
+
*
|
|
101
|
+
* Asset is FileAsset | GeneratorAsset. Multiple Clips can reference the same Asset.
|
|
102
|
+
* Assets never change their intrinsicDuration after registration.
|
|
103
|
+
*/
|
|
104
|
+
|
|
105
|
+
type AssetId = string & {
|
|
106
|
+
readonly __brand: 'AssetId';
|
|
107
|
+
};
|
|
108
|
+
declare const toAssetId: (s: string) => AssetId;
|
|
109
|
+
type AssetStatus = 'online' | 'offline' | 'proxy-only' | 'missing';
|
|
110
|
+
type FileAsset = {
|
|
111
|
+
readonly kind: 'file';
|
|
112
|
+
readonly id: AssetId;
|
|
113
|
+
readonly name: string;
|
|
114
|
+
readonly mediaType: TrackType;
|
|
115
|
+
readonly filePath: string;
|
|
116
|
+
readonly intrinsicDuration: TimelineFrame;
|
|
117
|
+
readonly nativeFps: FrameRate;
|
|
118
|
+
readonly sourceTimecodeOffset: TimelineFrame;
|
|
119
|
+
readonly status: AssetStatus;
|
|
120
|
+
};
|
|
121
|
+
type GeneratorAsset = {
|
|
122
|
+
readonly kind: 'generator';
|
|
123
|
+
readonly id: AssetId;
|
|
124
|
+
readonly name: string;
|
|
125
|
+
readonly mediaType: TrackType;
|
|
126
|
+
readonly intrinsicDuration: TimelineFrame;
|
|
127
|
+
readonly nativeFps: FrameRate;
|
|
128
|
+
readonly sourceTimecodeOffset: TimelineFrame;
|
|
129
|
+
readonly status: AssetStatus;
|
|
130
|
+
readonly generatorDef: Generator;
|
|
131
|
+
};
|
|
132
|
+
type Asset = FileAsset | GeneratorAsset;
|
|
133
|
+
declare function createAsset(params: {
|
|
134
|
+
id: string;
|
|
135
|
+
name: string;
|
|
136
|
+
mediaType: TrackType;
|
|
137
|
+
filePath: string;
|
|
138
|
+
intrinsicDuration: TimelineFrame;
|
|
139
|
+
nativeFps: FrameRate;
|
|
140
|
+
sourceTimecodeOffset: TimelineFrame;
|
|
141
|
+
status?: AssetStatus;
|
|
142
|
+
}): FileAsset;
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* EASING CURVES — Phase 4
|
|
146
|
+
*
|
|
147
|
+
* Discriminated union for keyframe/interpolation easing.
|
|
148
|
+
*/
|
|
149
|
+
type EasingCurve = {
|
|
150
|
+
readonly kind: 'Linear';
|
|
151
|
+
} | {
|
|
152
|
+
readonly kind: 'Hold';
|
|
153
|
+
} | {
|
|
154
|
+
readonly kind: 'EaseIn';
|
|
155
|
+
readonly power: number;
|
|
156
|
+
} | {
|
|
157
|
+
readonly kind: 'EaseOut';
|
|
158
|
+
readonly power: number;
|
|
159
|
+
} | {
|
|
160
|
+
readonly kind: 'EaseBoth';
|
|
161
|
+
readonly power: number;
|
|
162
|
+
} | {
|
|
163
|
+
readonly kind: 'BezierCurve';
|
|
164
|
+
readonly p1x: number;
|
|
165
|
+
readonly p1y: number;
|
|
166
|
+
readonly p2x: number;
|
|
167
|
+
readonly p2y: number;
|
|
168
|
+
};
|
|
169
|
+
declare const LINEAR_EASING: EasingCurve;
|
|
170
|
+
declare const HOLD_EASING: EasingCurve;
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* KEYFRAME MODEL — Phase 4
|
|
174
|
+
*
|
|
175
|
+
* Single keyframe for animatable values (effect params, transform, audio).
|
|
176
|
+
*/
|
|
177
|
+
|
|
178
|
+
type KeyframeId = string & {
|
|
179
|
+
readonly __brand: 'KeyframeId';
|
|
180
|
+
};
|
|
181
|
+
declare function toKeyframeId(s: string): KeyframeId;
|
|
182
|
+
/** Value is a plain number (opacity, scale, rotation, gain, etc.). */
|
|
183
|
+
type Keyframe = {
|
|
184
|
+
readonly id: KeyframeId;
|
|
185
|
+
readonly frame: TimelineFrame;
|
|
186
|
+
readonly value: number;
|
|
187
|
+
/** Easing out of this keyframe. */
|
|
188
|
+
readonly easing: EasingCurve;
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* EFFECT MODEL — Phase 4
|
|
193
|
+
*
|
|
194
|
+
* Effect applied to a clip (blur, LUT, color correct, host-defined).
|
|
195
|
+
*/
|
|
196
|
+
|
|
197
|
+
type EffectId = string & {
|
|
198
|
+
readonly __brand: 'EffectId';
|
|
199
|
+
};
|
|
200
|
+
declare function toEffectId(s: string): EffectId;
|
|
201
|
+
/** Open string: 'blur', 'lut', 'colorCorrect', host-defined. */
|
|
202
|
+
type EffectType = string;
|
|
203
|
+
type RenderStage = 'preComposite' | 'postComposite' | 'output';
|
|
204
|
+
type EffectParam = {
|
|
205
|
+
readonly key: string;
|
|
206
|
+
readonly value: number | string | boolean;
|
|
207
|
+
};
|
|
208
|
+
type Effect = {
|
|
209
|
+
readonly id: EffectId;
|
|
210
|
+
readonly effectType: EffectType;
|
|
211
|
+
readonly enabled: boolean;
|
|
212
|
+
readonly renderStage: RenderStage;
|
|
213
|
+
readonly params: readonly EffectParam[];
|
|
214
|
+
readonly keyframes: readonly Keyframe[];
|
|
215
|
+
};
|
|
216
|
+
declare function createEffect(id: EffectId, effectType: EffectType, renderStage?: RenderStage, params?: readonly EffectParam[]): Effect;
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* CLIP TRANSFORM — Phase 4
|
|
220
|
+
*
|
|
221
|
+
* Animatable position, scale, rotation, opacity, anchor.
|
|
222
|
+
* Each property: base value + optional keyframes.
|
|
223
|
+
*/
|
|
224
|
+
|
|
225
|
+
type AnimatableProperty = {
|
|
226
|
+
readonly value: number;
|
|
227
|
+
readonly keyframes: readonly Keyframe[];
|
|
228
|
+
};
|
|
229
|
+
declare function createAnimatableProperty(value: number): AnimatableProperty;
|
|
230
|
+
type ClipTransform = {
|
|
231
|
+
readonly positionX: AnimatableProperty;
|
|
232
|
+
readonly positionY: AnimatableProperty;
|
|
233
|
+
readonly scaleX: AnimatableProperty;
|
|
234
|
+
readonly scaleY: AnimatableProperty;
|
|
235
|
+
readonly rotation: AnimatableProperty;
|
|
236
|
+
readonly opacity: AnimatableProperty;
|
|
237
|
+
readonly anchorX: AnimatableProperty;
|
|
238
|
+
readonly anchorY: AnimatableProperty;
|
|
239
|
+
};
|
|
240
|
+
declare const DEFAULT_CLIP_TRANSFORM: ClipTransform;
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* AUDIO PROPERTIES — Phase 4
|
|
244
|
+
*
|
|
245
|
+
* Per-clip audio: gain, pan, mute, channel routing.
|
|
246
|
+
*/
|
|
247
|
+
|
|
248
|
+
type ChannelRouting = 'stereo' | 'mono' | 'left' | 'right';
|
|
249
|
+
type AudioProperties = {
|
|
250
|
+
readonly gain: AnimatableProperty;
|
|
251
|
+
readonly pan: AnimatableProperty;
|
|
252
|
+
readonly mute: boolean;
|
|
253
|
+
readonly channelRouting: ChannelRouting;
|
|
254
|
+
readonly normalizationGain: number;
|
|
255
|
+
};
|
|
256
|
+
declare const DEFAULT_AUDIO_PROPERTIES: AudioProperties;
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* TRANSITION MODEL — Phase 4
|
|
260
|
+
*
|
|
261
|
+
* Outgoing transition between clips (dissolve, wipe, etc.).
|
|
262
|
+
*/
|
|
263
|
+
|
|
264
|
+
type TransitionId = string & {
|
|
265
|
+
readonly __brand: 'TransitionId';
|
|
266
|
+
};
|
|
267
|
+
declare function toTransitionId(s: string): TransitionId;
|
|
268
|
+
/** 'dissolve' | 'wipe' | 'dip' | host-defined. */
|
|
269
|
+
type TransitionType = string;
|
|
270
|
+
type TransitionAlignment = 'centerOnCut' | 'endAtCut' | 'startAtCut';
|
|
271
|
+
type TransitionParam = {
|
|
272
|
+
readonly key: string;
|
|
273
|
+
readonly value: number | string | boolean;
|
|
274
|
+
};
|
|
275
|
+
type Transition = {
|
|
276
|
+
readonly id: TransitionId;
|
|
277
|
+
readonly type: TransitionType;
|
|
278
|
+
readonly durationFrames: number;
|
|
279
|
+
readonly alignment: TransitionAlignment;
|
|
280
|
+
readonly easing: EasingCurve;
|
|
281
|
+
readonly params: readonly TransitionParam[];
|
|
282
|
+
};
|
|
283
|
+
declare function createTransition(id: TransitionId, type: TransitionType, durationFrames: number, alignment?: TransitionAlignment, easing?: EasingCurve, params?: readonly TransitionParam[]): Transition;
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* CLIP MODEL — Phase 0 compliant
|
|
287
|
+
*
|
|
288
|
+
* A Clip is a time-bound reference to an Asset placed on a Track.
|
|
289
|
+
* All fields are readonly. Never mutate — always return a new object.
|
|
290
|
+
*/
|
|
291
|
+
|
|
292
|
+
type ClipId = string & {
|
|
293
|
+
readonly __brand: 'ClipId';
|
|
294
|
+
};
|
|
295
|
+
declare const toClipId: (s: string) => ClipId;
|
|
296
|
+
/**
|
|
297
|
+
* Clip — a time-bound viewport into an Asset on a Track.
|
|
298
|
+
*
|
|
299
|
+
* TIMELINE BOUNDS: timelineStart / timelineEnd — where it sits on the track.
|
|
300
|
+
* MEDIA BOUNDS: mediaIn / mediaOut — which portion of the asset plays.
|
|
301
|
+
*
|
|
302
|
+
* INVARIANTS (Phase 0, speed=1.0):
|
|
303
|
+
* timelineEnd > timelineStart
|
|
304
|
+
* mediaOut > mediaIn
|
|
305
|
+
* (mediaOut - mediaIn) === (timelineEnd - timelineStart)
|
|
306
|
+
* mediaIn >= 0
|
|
307
|
+
* mediaOut <= asset.intrinsicDuration
|
|
308
|
+
* timelineEnd <= timeline.duration
|
|
309
|
+
* speed > 0
|
|
310
|
+
*/
|
|
311
|
+
type Clip = {
|
|
312
|
+
readonly id: ClipId;
|
|
313
|
+
readonly assetId: AssetId;
|
|
314
|
+
readonly trackId: TrackId;
|
|
315
|
+
readonly timelineStart: TimelineFrame;
|
|
316
|
+
readonly timelineEnd: TimelineFrame;
|
|
317
|
+
readonly mediaIn: TimelineFrame;
|
|
318
|
+
readonly mediaOut: TimelineFrame;
|
|
319
|
+
readonly speed: number;
|
|
320
|
+
readonly enabled: boolean;
|
|
321
|
+
readonly reversed: boolean;
|
|
322
|
+
readonly name: string | null;
|
|
323
|
+
readonly color: string | null;
|
|
324
|
+
readonly metadata: Record<string, string>;
|
|
325
|
+
readonly effects?: readonly Effect[];
|
|
326
|
+
readonly transform?: ClipTransform;
|
|
327
|
+
readonly audio?: AudioProperties;
|
|
328
|
+
readonly transition?: Transition;
|
|
329
|
+
};
|
|
330
|
+
declare function createClip(params: {
|
|
331
|
+
id: string;
|
|
332
|
+
assetId: string;
|
|
333
|
+
trackId: string;
|
|
334
|
+
timelineStart: TimelineFrame;
|
|
335
|
+
timelineEnd: TimelineFrame;
|
|
336
|
+
mediaIn: TimelineFrame;
|
|
337
|
+
mediaOut: TimelineFrame;
|
|
338
|
+
speed?: number;
|
|
339
|
+
enabled?: boolean;
|
|
340
|
+
reversed?: boolean;
|
|
341
|
+
name?: string | null;
|
|
342
|
+
color?: string | null;
|
|
343
|
+
metadata?: Record<string, string>;
|
|
344
|
+
effects?: readonly Effect[];
|
|
345
|
+
transform?: ClipTransform;
|
|
346
|
+
audio?: AudioProperties;
|
|
347
|
+
transition?: Transition;
|
|
348
|
+
}): Clip;
|
|
349
|
+
declare function getClipDuration(clip: Clip): TimelineFrame;
|
|
350
|
+
declare function getClipMediaDuration(clip: Clip): TimelineFrame;
|
|
351
|
+
declare function clipContainsFrame(clip: Clip, f: TimelineFrame): boolean;
|
|
352
|
+
declare function clipsOverlap(a: Clip, b: Clip): boolean;
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* CAPTION MODEL — Phase 3
|
|
356
|
+
*
|
|
357
|
+
* Captions live on Track.captions[]. Used for SRT/VTT and burn-in.
|
|
358
|
+
*/
|
|
359
|
+
|
|
360
|
+
type CaptionId = string & {
|
|
361
|
+
readonly __brand: 'CaptionId';
|
|
362
|
+
};
|
|
363
|
+
type CaptionStyle = {
|
|
364
|
+
readonly fontFamily: string;
|
|
365
|
+
readonly fontSize: number;
|
|
366
|
+
readonly color: string;
|
|
367
|
+
readonly backgroundColor: string;
|
|
368
|
+
readonly hAlign: 'left' | 'center' | 'right';
|
|
369
|
+
readonly vAlign: 'top' | 'center' | 'bottom';
|
|
370
|
+
};
|
|
371
|
+
type Caption = {
|
|
372
|
+
readonly id: CaptionId;
|
|
373
|
+
readonly text: string;
|
|
374
|
+
readonly startFrame: TimelineFrame;
|
|
375
|
+
readonly endFrame: TimelineFrame;
|
|
376
|
+
readonly language: string;
|
|
377
|
+
readonly style: CaptionStyle;
|
|
378
|
+
readonly burnIn: boolean;
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* TRACK GROUP — Phase 4
|
|
383
|
+
*
|
|
384
|
+
* Logical grouping of tracks (e.g. for nesting or UI collapse).
|
|
385
|
+
*/
|
|
386
|
+
|
|
387
|
+
type TrackGroupId = string & {
|
|
388
|
+
readonly __brand: 'TrackGroupId';
|
|
389
|
+
};
|
|
390
|
+
declare function toTrackGroupId(s: string): TrackGroupId;
|
|
391
|
+
type TrackGroup = {
|
|
392
|
+
readonly id: TrackGroupId;
|
|
393
|
+
readonly label: string;
|
|
394
|
+
readonly trackIds: readonly TrackId[];
|
|
395
|
+
readonly collapsed: boolean;
|
|
396
|
+
};
|
|
397
|
+
declare function createTrackGroup(id: TrackGroupId, label: string, trackIds?: readonly TrackId[]): TrackGroup;
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* TRACK MODEL — Phase 0 + Phase 3
|
|
401
|
+
*
|
|
402
|
+
* A Track is a horizontal container for Clips, always sorted by timelineStart.
|
|
403
|
+
* Phase 3: captions[] for subtitle/caption items.
|
|
404
|
+
*/
|
|
405
|
+
|
|
406
|
+
type TrackId = string & {
|
|
407
|
+
readonly __brand: 'TrackId';
|
|
408
|
+
};
|
|
409
|
+
declare const toTrackId: (s: string) => TrackId;
|
|
410
|
+
type TrackType = 'video' | 'audio' | 'subtitle' | 'title';
|
|
411
|
+
type Track = {
|
|
412
|
+
readonly id: TrackId;
|
|
413
|
+
readonly name: string;
|
|
414
|
+
readonly type: TrackType;
|
|
415
|
+
readonly locked: boolean;
|
|
416
|
+
readonly muted: boolean;
|
|
417
|
+
readonly solo: boolean;
|
|
418
|
+
readonly height: number;
|
|
419
|
+
/** Always sorted ascending by timelineStart — invariant enforced by checkInvariants. */
|
|
420
|
+
readonly clips: readonly Clip[];
|
|
421
|
+
/** Phase 3: captions on this track (e.g. subtitle/title). */
|
|
422
|
+
readonly captions: readonly Caption[];
|
|
423
|
+
readonly blendMode?: string;
|
|
424
|
+
readonly opacity?: number;
|
|
425
|
+
readonly groupId?: TrackGroupId;
|
|
426
|
+
};
|
|
427
|
+
declare function createTrack(params: {
|
|
428
|
+
id: string;
|
|
429
|
+
name: string;
|
|
430
|
+
type: TrackType;
|
|
431
|
+
clips?: readonly Clip[];
|
|
432
|
+
captions?: readonly Caption[];
|
|
433
|
+
locked?: boolean;
|
|
434
|
+
muted?: boolean;
|
|
435
|
+
solo?: boolean;
|
|
436
|
+
height?: number;
|
|
437
|
+
blendMode?: string;
|
|
438
|
+
opacity?: number;
|
|
439
|
+
groupId?: TrackGroupId;
|
|
440
|
+
}): Track;
|
|
441
|
+
/** Returns a new track with clips sorted ascending by timelineStart. */
|
|
442
|
+
declare function sortTrackClips(track: Track): Track;
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* MARKER TYPES — Phase 3
|
|
446
|
+
*
|
|
447
|
+
* Discriminated union: point (single frame) or range (frameStart..frameEnd).
|
|
448
|
+
* Markers live on Timeline.markers[]. linkedClipId moves with clip on ripple.
|
|
449
|
+
*/
|
|
450
|
+
|
|
451
|
+
type MarkerId = string & {
|
|
452
|
+
readonly __brand: 'MarkerId';
|
|
453
|
+
};
|
|
454
|
+
type MarkerScope = 'global' | 'personal' | 'export';
|
|
455
|
+
type Marker = {
|
|
456
|
+
readonly type: 'point';
|
|
457
|
+
readonly id: MarkerId;
|
|
458
|
+
readonly frame: TimelineFrame;
|
|
459
|
+
readonly label: string;
|
|
460
|
+
readonly color: string;
|
|
461
|
+
readonly scope: MarkerScope;
|
|
462
|
+
readonly linkedClipId: ClipId | null;
|
|
463
|
+
readonly clipId?: ClipId;
|
|
464
|
+
} | {
|
|
465
|
+
readonly type: 'range';
|
|
466
|
+
readonly id: MarkerId;
|
|
467
|
+
readonly frameStart: TimelineFrame;
|
|
468
|
+
readonly frameEnd: TimelineFrame;
|
|
469
|
+
readonly label: string;
|
|
470
|
+
readonly color: string;
|
|
471
|
+
readonly scope: MarkerScope;
|
|
472
|
+
readonly linkedClipId: ClipId | null;
|
|
473
|
+
readonly clipId?: ClipId;
|
|
474
|
+
};
|
|
475
|
+
type BeatGrid = {
|
|
476
|
+
readonly bpm: number;
|
|
477
|
+
readonly timeSignature: readonly [number, number];
|
|
478
|
+
readonly offset: TimelineFrame;
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* LINK GROUP — Phase 4
|
|
483
|
+
*
|
|
484
|
+
* Locks A/V clips in sync; when one moves, all move together.
|
|
485
|
+
*/
|
|
486
|
+
|
|
487
|
+
type LinkGroupId = string & {
|
|
488
|
+
readonly __brand: 'LinkGroupId';
|
|
489
|
+
};
|
|
490
|
+
declare function toLinkGroupId(s: string): LinkGroupId;
|
|
491
|
+
type LinkGroup = {
|
|
492
|
+
readonly id: LinkGroupId;
|
|
493
|
+
/** Min 2 clips. */
|
|
494
|
+
readonly clipIds: readonly ClipId[];
|
|
495
|
+
};
|
|
496
|
+
declare function createLinkGroup(id: LinkGroupId, clipIds: readonly ClipId[]): LinkGroup;
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* TIMELINE MODEL — Phase 0 + Phase 3
|
|
500
|
+
*/
|
|
501
|
+
|
|
502
|
+
type SequenceSettings = {
|
|
503
|
+
readonly pixelAspectRatio: number;
|
|
504
|
+
readonly fieldOrder: 'progressive' | 'upper' | 'lower';
|
|
505
|
+
readonly colorSpace: string;
|
|
506
|
+
readonly audioSampleRate: number;
|
|
507
|
+
readonly audioChannelCount: number;
|
|
508
|
+
};
|
|
509
|
+
type Timeline = {
|
|
510
|
+
readonly id: string;
|
|
511
|
+
readonly name: string;
|
|
512
|
+
readonly fps: FrameRate;
|
|
513
|
+
readonly duration: TimelineFrame;
|
|
514
|
+
readonly startTimecode: Timecode;
|
|
515
|
+
readonly tracks: readonly Track[];
|
|
516
|
+
readonly sequenceSettings: SequenceSettings;
|
|
517
|
+
/**
|
|
518
|
+
* Increments by 1 on every successfully committed Transaction.
|
|
519
|
+
* Use this to detect stale references without deep equality checks.
|
|
520
|
+
*/
|
|
521
|
+
readonly version: number;
|
|
522
|
+
readonly markers: readonly Marker[];
|
|
523
|
+
readonly beatGrid: BeatGrid | null;
|
|
524
|
+
readonly inPoint: TimelineFrame | null;
|
|
525
|
+
readonly outPoint: TimelineFrame | null;
|
|
526
|
+
readonly trackGroups?: readonly TrackGroup[];
|
|
527
|
+
readonly linkGroups?: readonly LinkGroup[];
|
|
528
|
+
};
|
|
529
|
+
declare function createTimeline(params: {
|
|
530
|
+
id: string;
|
|
531
|
+
name: string;
|
|
532
|
+
fps: FrameRate;
|
|
533
|
+
duration: TimelineFrame;
|
|
534
|
+
startTimecode?: Timecode;
|
|
535
|
+
tracks?: readonly Track[];
|
|
536
|
+
sequenceSettings?: Partial<SequenceSettings>;
|
|
537
|
+
markers?: readonly Marker[];
|
|
538
|
+
beatGrid?: BeatGrid | null;
|
|
539
|
+
inPoint?: TimelineFrame | null;
|
|
540
|
+
outPoint?: TimelineFrame | null;
|
|
541
|
+
trackGroups?: readonly TrackGroup[];
|
|
542
|
+
linkGroups?: readonly LinkGroup[];
|
|
543
|
+
}): Timeline;
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* TIMELINE STATE — Phase 0 compliant
|
|
547
|
+
*
|
|
548
|
+
* TimelineState is the single source of truth for the engine.
|
|
549
|
+
* Phase 0 only: timeline + assetRegistry. No Phase 2 fields.
|
|
550
|
+
*
|
|
551
|
+
* RULE: Every function that changes state returns a NEW TimelineState.
|
|
552
|
+
* Never mutate the existing state.
|
|
553
|
+
*/
|
|
554
|
+
|
|
555
|
+
type AssetRegistry = ReadonlyMap<AssetId, Asset>;
|
|
556
|
+
/**
|
|
557
|
+
* Increment this whenever TimelineState gains a new required field or
|
|
558
|
+
* a field's semantics change in a breaking way.
|
|
559
|
+
*
|
|
560
|
+
* The schemaVersion invariant check rejects loading a future schema
|
|
561
|
+
* into an older engine (prevents silent data corruption on downgrade).
|
|
562
|
+
*/
|
|
563
|
+
declare const CURRENT_SCHEMA_VERSION: 2;
|
|
564
|
+
type TimelineState = {
|
|
565
|
+
readonly schemaVersion: number;
|
|
566
|
+
readonly timeline: Timeline;
|
|
567
|
+
readonly assetRegistry: AssetRegistry;
|
|
568
|
+
};
|
|
569
|
+
declare function createTimelineState(params: {
|
|
570
|
+
timeline: Timeline;
|
|
571
|
+
assetRegistry?: AssetRegistry;
|
|
572
|
+
/** @deprecated use assetRegistry. Kept for test backward-compat only. */
|
|
573
|
+
assets?: Map<string, Asset>;
|
|
574
|
+
}): TimelineState;
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* FRAME UTILITIES
|
|
578
|
+
*
|
|
579
|
+
* Pure functions for working with frame-based time values.
|
|
580
|
+
*
|
|
581
|
+
* These utilities handle:
|
|
582
|
+
* - Converting between frames and seconds
|
|
583
|
+
* - Formatting frames as timecode (HH:MM:SS:FF)
|
|
584
|
+
* - Frame arithmetic (clamping, rounding)
|
|
585
|
+
*
|
|
586
|
+
* CRITICAL RULES:
|
|
587
|
+
* - All conversions must quantize to whole frames
|
|
588
|
+
* - No floating-point frame values allowed
|
|
589
|
+
* - Always round/floor/ceil explicitly
|
|
590
|
+
*
|
|
591
|
+
* USAGE:
|
|
592
|
+
* ```typescript
|
|
593
|
+
* const fps = frameRate(30);
|
|
594
|
+
* const frames = secondsToFrames(5.5, fps); // 165 frames
|
|
595
|
+
* const seconds = framesToSeconds(frames, fps); // 5.5 seconds
|
|
596
|
+
* const timecode = framesToTimecode(frames, fps); // "00:00:05:15"
|
|
597
|
+
* ```
|
|
598
|
+
*/
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Convert frames to seconds
|
|
602
|
+
*
|
|
603
|
+
* @param frames - Frame number
|
|
604
|
+
* @param fps - Frames per second
|
|
605
|
+
* @returns Time in seconds (may be fractional)
|
|
606
|
+
*/
|
|
607
|
+
declare function framesToSeconds(frames: TimelineFrame, fps: FrameRate): number;
|
|
608
|
+
/**
|
|
609
|
+
* Convert seconds to frames
|
|
610
|
+
*
|
|
611
|
+
* IMPORTANT: This rounds to the nearest frame.
|
|
612
|
+
* If you need different rounding behavior, use Math.floor or Math.ceil explicitly.
|
|
613
|
+
*
|
|
614
|
+
* @param seconds - Time in seconds
|
|
615
|
+
* @param fps - Frames per second
|
|
616
|
+
* @returns Frame number (rounded to nearest frame)
|
|
617
|
+
*/
|
|
618
|
+
declare function secondsToFrames(seconds: number, fps: FrameRate): TimelineFrame;
|
|
619
|
+
/**
|
|
620
|
+
* Convert frames to timecode format (HH:MM:SS:FF)
|
|
621
|
+
*
|
|
622
|
+
* Example: 3825 frames at 30fps = "00:02:07:15"
|
|
623
|
+
*
|
|
624
|
+
* @param frames - Frame number
|
|
625
|
+
* @param fps - Frames per second
|
|
626
|
+
* @returns Timecode string
|
|
627
|
+
*/
|
|
628
|
+
declare function framesToTimecode(frames: TimelineFrame, fps: FrameRate): string;
|
|
629
|
+
/**
|
|
630
|
+
* Convert frames to simple MM:SS format
|
|
631
|
+
*
|
|
632
|
+
* Example: 3825 frames at 30fps = "02:07"
|
|
633
|
+
*
|
|
634
|
+
* @param frames - Frame number
|
|
635
|
+
* @param fps - Frames per second
|
|
636
|
+
* @returns Time string in MM:SS format
|
|
637
|
+
*/
|
|
638
|
+
declare function framesToMinutesSeconds(frames: TimelineFrame, fps: FrameRate): string;
|
|
639
|
+
/**
|
|
640
|
+
* Clamp a frame value between min and max
|
|
641
|
+
*
|
|
642
|
+
* @param value - Frame to clamp
|
|
643
|
+
* @param min - Minimum frame (inclusive)
|
|
644
|
+
* @param max - Maximum frame (inclusive)
|
|
645
|
+
* @returns Clamped frame value
|
|
646
|
+
*/
|
|
647
|
+
declare function clampFrame(value: TimelineFrame, min: TimelineFrame, max: TimelineFrame): TimelineFrame;
|
|
648
|
+
/**
|
|
649
|
+
* Add two frame values
|
|
650
|
+
*
|
|
651
|
+
* @param a - First frame
|
|
652
|
+
* @param b - Second frame
|
|
653
|
+
* @returns Sum of frames
|
|
654
|
+
*/
|
|
655
|
+
declare function addFrames(a: TimelineFrame, b: TimelineFrame): TimelineFrame;
|
|
656
|
+
/**
|
|
657
|
+
* Subtract two frame values
|
|
658
|
+
*
|
|
659
|
+
* @param a - First frame
|
|
660
|
+
* @param b - Second frame (subtracted from a)
|
|
661
|
+
* @returns Difference of frames (clamped to 0 if negative)
|
|
662
|
+
*/
|
|
663
|
+
declare function subtractFrames(a: TimelineFrame, b: TimelineFrame): TimelineFrame;
|
|
664
|
+
/**
|
|
665
|
+
* Calculate duration between two frames
|
|
666
|
+
*
|
|
667
|
+
* @param start - Start frame
|
|
668
|
+
* @param end - End frame
|
|
669
|
+
* @returns Duration in frames (end - start)
|
|
670
|
+
*/
|
|
671
|
+
declare function frameDuration(start: TimelineFrame, end: TimelineFrame): TimelineFrame;
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* TIMELINE ENGINE
|
|
675
|
+
*
|
|
676
|
+
* The main public API for the timeline editing kernel.
|
|
677
|
+
*
|
|
678
|
+
* WHAT IS THE TIMELINE ENGINE?
|
|
679
|
+
* - A thin wrapper around the history and dispatch systems
|
|
680
|
+
* - Provides a convenient, object-oriented API
|
|
681
|
+
* - Manages internal state
|
|
682
|
+
* - Coordinates operations, validation, and history
|
|
683
|
+
*
|
|
684
|
+
* WHY A CLASS?
|
|
685
|
+
* - Encapsulates state management
|
|
686
|
+
* - Provides a clean API for users
|
|
687
|
+
* - Hides complexity of history and dispatch
|
|
688
|
+
* - Familiar OOP interface for most developers
|
|
689
|
+
*
|
|
690
|
+
* USAGE:
|
|
691
|
+
* ```typescript
|
|
692
|
+
* const engine = new TimelineEngine(initialState);
|
|
693
|
+
*
|
|
694
|
+
* // Add a clip
|
|
695
|
+
* const result = engine.addClip(trackId, clip);
|
|
696
|
+
* if (!result.success) {
|
|
697
|
+
* console.error('Failed to add clip:', result.errors);
|
|
698
|
+
* }
|
|
699
|
+
*
|
|
700
|
+
* // Undo/redo
|
|
701
|
+
* engine.undo();
|
|
702
|
+
* engine.redo();
|
|
703
|
+
*
|
|
704
|
+
* // Query state
|
|
705
|
+
* const clip = engine.findClipById('clip_1');
|
|
706
|
+
* const state = engine.getState();
|
|
707
|
+
* ```
|
|
708
|
+
*
|
|
709
|
+
* DESIGN PHILOSOPHY:
|
|
710
|
+
* - Business logic lives in pure modules (operations, validation, etc.)
|
|
711
|
+
* - Engine is just a thin orchestration layer
|
|
712
|
+
* - Easy to test (can test pure functions independently)
|
|
713
|
+
*/
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* TimelineEngine - The main timeline editing engine
|
|
717
|
+
*
|
|
718
|
+
* Provides a high-level API for timeline editing with built-in
|
|
719
|
+
* undo/redo, validation, and state management.
|
|
720
|
+
*/
|
|
721
|
+
declare class TimelineEngine {
|
|
722
|
+
private history;
|
|
723
|
+
private listeners;
|
|
724
|
+
/**
|
|
725
|
+
* Create a new timeline engine
|
|
726
|
+
*
|
|
727
|
+
* @param initialState - Initial timeline state
|
|
728
|
+
* @param historyLimit - Maximum number of undo steps (default: 50)
|
|
729
|
+
*/
|
|
730
|
+
constructor(initialState: TimelineState, historyLimit?: number);
|
|
731
|
+
/**
|
|
732
|
+
* Subscribe to state changes
|
|
733
|
+
*
|
|
734
|
+
* The listener will be called whenever the timeline state changes,
|
|
735
|
+
* with the new state passed as an argument.
|
|
736
|
+
* This is used by framework adapters (e.g., React) to trigger re-renders.
|
|
737
|
+
*
|
|
738
|
+
* @param listener - Function to call on state changes, receives new state
|
|
739
|
+
* @returns Unsubscribe function
|
|
740
|
+
*
|
|
741
|
+
* @example
|
|
742
|
+
* ```typescript
|
|
743
|
+
* const unsubscribe = engine.subscribe((state) => {
|
|
744
|
+
* console.log('State changed:', state);
|
|
745
|
+
* });
|
|
746
|
+
*
|
|
747
|
+
* // Later...
|
|
748
|
+
* unsubscribe();
|
|
749
|
+
* ```
|
|
750
|
+
*/
|
|
751
|
+
subscribe(listener: (state: TimelineState) => void): () => void;
|
|
752
|
+
/**
|
|
753
|
+
* Notify all subscribers of a state change
|
|
754
|
+
*
|
|
755
|
+
* This is called internally after any operation that modifies state.
|
|
756
|
+
* Framework adapters use this to trigger re-renders.
|
|
757
|
+
*/
|
|
758
|
+
private notify;
|
|
759
|
+
/**
|
|
760
|
+
* Get the current timeline state
|
|
761
|
+
*
|
|
762
|
+
* @returns Current timeline state
|
|
763
|
+
*/
|
|
764
|
+
getState(): TimelineState;
|
|
765
|
+
/**
|
|
766
|
+
* Register an asset
|
|
767
|
+
*
|
|
768
|
+
* @param asset - Asset to register
|
|
769
|
+
* @returns Dispatch result
|
|
770
|
+
*/
|
|
771
|
+
registerAsset(asset: Asset): {
|
|
772
|
+
accepted: boolean;
|
|
773
|
+
errors?: {
|
|
774
|
+
code: string;
|
|
775
|
+
message: string;
|
|
776
|
+
}[];
|
|
777
|
+
};
|
|
778
|
+
/**
|
|
779
|
+
* Get an asset by ID
|
|
780
|
+
*
|
|
781
|
+
* @param assetId - Asset ID
|
|
782
|
+
* @returns The asset, or undefined if not found
|
|
783
|
+
*/
|
|
784
|
+
getAsset(assetId: string): Asset | undefined;
|
|
785
|
+
/**
|
|
786
|
+
* Add a clip to a track
|
|
787
|
+
*
|
|
788
|
+
* @param trackId - ID of the track to add to
|
|
789
|
+
* @param clip - Clip to add
|
|
790
|
+
* @returns Dispatch result
|
|
791
|
+
*/
|
|
792
|
+
addClip(trackId: string, clip: Clip): {
|
|
793
|
+
accepted: boolean;
|
|
794
|
+
errors?: {
|
|
795
|
+
code: string;
|
|
796
|
+
message: string;
|
|
797
|
+
}[];
|
|
798
|
+
};
|
|
799
|
+
/**
|
|
800
|
+
* Remove a clip
|
|
801
|
+
*
|
|
802
|
+
* @param clipId - ID of the clip to remove
|
|
803
|
+
* @returns Dispatch result
|
|
804
|
+
*/
|
|
805
|
+
removeClip(clipId: string): {
|
|
806
|
+
accepted: boolean;
|
|
807
|
+
errors?: {
|
|
808
|
+
code: string;
|
|
809
|
+
message: string;
|
|
810
|
+
}[];
|
|
811
|
+
};
|
|
812
|
+
/**
|
|
813
|
+
* Move a clip to a new timeline position
|
|
814
|
+
*
|
|
815
|
+
* @param clipId - ID of the clip to move
|
|
816
|
+
* @param newStart - New timeline start frame
|
|
817
|
+
* @returns Dispatch result
|
|
818
|
+
*/
|
|
819
|
+
moveClip(clipId: string, newStart: TimelineFrame): {
|
|
820
|
+
accepted: boolean;
|
|
821
|
+
errors?: {
|
|
822
|
+
code: string;
|
|
823
|
+
message: string;
|
|
824
|
+
}[];
|
|
825
|
+
};
|
|
826
|
+
/**
|
|
827
|
+
* Resize a clip
|
|
828
|
+
*
|
|
829
|
+
* @param clipId - ID of the clip to resize
|
|
830
|
+
* @param newStart - New timeline start frame
|
|
831
|
+
* @param newEnd - New timeline end frame
|
|
832
|
+
* @returns Dispatch result
|
|
833
|
+
*/
|
|
834
|
+
resizeClip(clipId: string, newStart: TimelineFrame, newEnd: TimelineFrame): {
|
|
835
|
+
accepted: boolean;
|
|
836
|
+
errors?: {
|
|
837
|
+
code: string;
|
|
838
|
+
message: string;
|
|
839
|
+
}[];
|
|
840
|
+
};
|
|
841
|
+
/**
|
|
842
|
+
* Trim a clip (change media bounds)
|
|
843
|
+
*
|
|
844
|
+
* @param clipId - ID of the clip to trim
|
|
845
|
+
* @param newMediaIn - New media in frame
|
|
846
|
+
* @param newMediaOut - New media out frame
|
|
847
|
+
* @returns Dispatch result
|
|
848
|
+
*/
|
|
849
|
+
trimClip(clipId: string, newMediaIn: TimelineFrame, newMediaOut: TimelineFrame): {
|
|
850
|
+
accepted: boolean;
|
|
851
|
+
errors?: {
|
|
852
|
+
code: string;
|
|
853
|
+
message: string;
|
|
854
|
+
}[];
|
|
855
|
+
};
|
|
856
|
+
/**
|
|
857
|
+
* Move a clip to a different track
|
|
858
|
+
*
|
|
859
|
+
* @param clipId - ID of the clip to move
|
|
860
|
+
* @param targetTrackId - ID of the target track
|
|
861
|
+
* @returns Dispatch result
|
|
862
|
+
*/
|
|
863
|
+
moveClipToTrack(clipId: string, targetTrackId: string): {
|
|
864
|
+
accepted: boolean;
|
|
865
|
+
errors?: {
|
|
866
|
+
code: string;
|
|
867
|
+
message: string;
|
|
868
|
+
}[];
|
|
869
|
+
};
|
|
870
|
+
/**
|
|
871
|
+
* Add a track
|
|
872
|
+
*
|
|
873
|
+
* @param track - Track to add
|
|
874
|
+
* @returns Dispatch result
|
|
875
|
+
*/
|
|
876
|
+
addTrack(track: Track): {
|
|
877
|
+
accepted: boolean;
|
|
878
|
+
errors?: {
|
|
879
|
+
code: string;
|
|
880
|
+
message: string;
|
|
881
|
+
}[];
|
|
882
|
+
};
|
|
883
|
+
/**
|
|
884
|
+
* Remove a track
|
|
885
|
+
*
|
|
886
|
+
* @param trackId - ID of the track to remove
|
|
887
|
+
* @returns Dispatch result
|
|
888
|
+
*/
|
|
889
|
+
removeTrack(trackId: string): {
|
|
890
|
+
accepted: boolean;
|
|
891
|
+
errors?: {
|
|
892
|
+
code: string;
|
|
893
|
+
message: string;
|
|
894
|
+
}[];
|
|
895
|
+
};
|
|
896
|
+
/**
|
|
897
|
+
* Move a track to a new position
|
|
898
|
+
*
|
|
899
|
+
* @param trackId - ID of the track to move
|
|
900
|
+
* @param newIndex - New index position
|
|
901
|
+
* @returns Dispatch result
|
|
902
|
+
*/
|
|
903
|
+
moveTrack(trackId: string, newIndex: number): {
|
|
904
|
+
accepted: boolean;
|
|
905
|
+
errors?: {
|
|
906
|
+
code: string;
|
|
907
|
+
message: string;
|
|
908
|
+
}[];
|
|
909
|
+
};
|
|
910
|
+
/**
|
|
911
|
+
* Toggle track mute
|
|
912
|
+
*
|
|
913
|
+
* @param trackId - ID of the track
|
|
914
|
+
* @returns Dispatch result
|
|
915
|
+
*/
|
|
916
|
+
toggleTrackMute(trackId: string): {
|
|
917
|
+
accepted: boolean;
|
|
918
|
+
errors?: {
|
|
919
|
+
code: string;
|
|
920
|
+
message: string;
|
|
921
|
+
}[];
|
|
922
|
+
};
|
|
923
|
+
/**
|
|
924
|
+
* Toggle track lock
|
|
925
|
+
*
|
|
926
|
+
* @param trackId - ID of the track
|
|
927
|
+
* @returns Dispatch result
|
|
928
|
+
*/
|
|
929
|
+
toggleTrackLock(trackId: string): {
|
|
930
|
+
accepted: boolean;
|
|
931
|
+
errors?: {
|
|
932
|
+
code: string;
|
|
933
|
+
message: string;
|
|
934
|
+
}[];
|
|
935
|
+
};
|
|
936
|
+
/**
|
|
937
|
+
* Toggle track solo
|
|
938
|
+
*
|
|
939
|
+
* @param trackId - ID of the track
|
|
940
|
+
* @returns Dispatch result
|
|
941
|
+
*/
|
|
942
|
+
toggleTrackSolo(trackId: string): {
|
|
943
|
+
accepted: boolean;
|
|
944
|
+
errors?: {
|
|
945
|
+
code: string;
|
|
946
|
+
message: string;
|
|
947
|
+
}[];
|
|
948
|
+
};
|
|
949
|
+
/**
|
|
950
|
+
* Set track height
|
|
951
|
+
*
|
|
952
|
+
* @param trackId - ID of the track
|
|
953
|
+
* @param height - New height in pixels
|
|
954
|
+
* @returns Dispatch result
|
|
955
|
+
*/
|
|
956
|
+
setTrackHeight(trackId: string, height: number): {
|
|
957
|
+
accepted: boolean;
|
|
958
|
+
errors?: {
|
|
959
|
+
code: string;
|
|
960
|
+
message: string;
|
|
961
|
+
}[];
|
|
962
|
+
};
|
|
963
|
+
/**
|
|
964
|
+
* Set timeline duration
|
|
965
|
+
*
|
|
966
|
+
* @param duration - New duration in frames
|
|
967
|
+
* @returns Dispatch result
|
|
968
|
+
*/
|
|
969
|
+
setTimelineDuration(duration: TimelineFrame): {
|
|
970
|
+
accepted: boolean;
|
|
971
|
+
errors?: {
|
|
972
|
+
code: string;
|
|
973
|
+
message: string;
|
|
974
|
+
}[];
|
|
975
|
+
};
|
|
976
|
+
/**
|
|
977
|
+
* Set timeline name
|
|
978
|
+
*
|
|
979
|
+
* @param name - New timeline name
|
|
980
|
+
* @returns Dispatch result
|
|
981
|
+
*/
|
|
982
|
+
setTimelineName(name: string): {
|
|
983
|
+
accepted: boolean;
|
|
984
|
+
errors?: {
|
|
985
|
+
code: string;
|
|
986
|
+
message: string;
|
|
987
|
+
}[];
|
|
988
|
+
};
|
|
989
|
+
/**
|
|
990
|
+
* Undo the last action
|
|
991
|
+
*
|
|
992
|
+
* @returns true if undo was performed
|
|
993
|
+
*/
|
|
994
|
+
undo(): boolean;
|
|
995
|
+
/**
|
|
996
|
+
* Redo the last undone action
|
|
997
|
+
*
|
|
998
|
+
* @returns true if redo was performed
|
|
999
|
+
*/
|
|
1000
|
+
redo(): boolean;
|
|
1001
|
+
/**
|
|
1002
|
+
* Check if undo is available
|
|
1003
|
+
*
|
|
1004
|
+
* @returns true if undo is available
|
|
1005
|
+
*/
|
|
1006
|
+
canUndo(): boolean;
|
|
1007
|
+
/**
|
|
1008
|
+
* Check if redo is available
|
|
1009
|
+
*
|
|
1010
|
+
* @returns true if redo is available
|
|
1011
|
+
*/
|
|
1012
|
+
canRedo(): boolean;
|
|
1013
|
+
/**
|
|
1014
|
+
* Find a clip by ID
|
|
1015
|
+
*
|
|
1016
|
+
* @param clipId - Clip ID
|
|
1017
|
+
* @returns The clip, or undefined if not found
|
|
1018
|
+
*/
|
|
1019
|
+
findClipById(clipId: string): Clip | undefined;
|
|
1020
|
+
/**
|
|
1021
|
+
* Find a track by ID
|
|
1022
|
+
*
|
|
1023
|
+
* @param trackId - Track ID
|
|
1024
|
+
* @returns The track, or undefined if not found
|
|
1025
|
+
*/
|
|
1026
|
+
findTrackById(trackId: string): Track | undefined;
|
|
1027
|
+
/**
|
|
1028
|
+
* Get all clips on a track
|
|
1029
|
+
*
|
|
1030
|
+
* @param trackId - Track ID
|
|
1031
|
+
* @returns Array of clips on the track
|
|
1032
|
+
*/
|
|
1033
|
+
getClipsOnTrack(trackId: string): Clip[];
|
|
1034
|
+
/**
|
|
1035
|
+
* Get all clips at a specific frame
|
|
1036
|
+
*
|
|
1037
|
+
* @param frame - Frame to check
|
|
1038
|
+
* @returns Array of clips at that frame
|
|
1039
|
+
*/
|
|
1040
|
+
getClipsAtFrame(f: TimelineFrame): Clip[];
|
|
1041
|
+
/**
|
|
1042
|
+
* Get all clips in a frame range
|
|
1043
|
+
*
|
|
1044
|
+
* @param start - Start frame
|
|
1045
|
+
* @param end - End frame
|
|
1046
|
+
* @returns Array of clips in the range
|
|
1047
|
+
*/
|
|
1048
|
+
getClipsInRange(start: TimelineFrame, end: TimelineFrame): Clip[];
|
|
1049
|
+
/**
|
|
1050
|
+
* Get all clips in the timeline
|
|
1051
|
+
*
|
|
1052
|
+
* @returns Array of all clips
|
|
1053
|
+
*/
|
|
1054
|
+
getAllClips(): Clip[];
|
|
1055
|
+
/**
|
|
1056
|
+
* Get all tracks in the timeline
|
|
1057
|
+
*
|
|
1058
|
+
* @returns Array of all tracks
|
|
1059
|
+
*/
|
|
1060
|
+
getAllTracks(): readonly Track[];
|
|
1061
|
+
/**
|
|
1062
|
+
* Ripple delete - delete clip and shift subsequent clips left
|
|
1063
|
+
*
|
|
1064
|
+
* @param clipId - ID of the clip to delete
|
|
1065
|
+
* @returns Dispatch result
|
|
1066
|
+
*/
|
|
1067
|
+
rippleDelete(clipId: string): {
|
|
1068
|
+
accepted: boolean;
|
|
1069
|
+
errors?: {
|
|
1070
|
+
code: string;
|
|
1071
|
+
message: string;
|
|
1072
|
+
}[];
|
|
1073
|
+
};
|
|
1074
|
+
/**
|
|
1075
|
+
* Ripple trim - trim clip end and shift subsequent clips
|
|
1076
|
+
*
|
|
1077
|
+
* @param clipId - ID of the clip to trim
|
|
1078
|
+
* @param newEnd - New end frame for the clip
|
|
1079
|
+
* @returns Dispatch result
|
|
1080
|
+
*/
|
|
1081
|
+
rippleTrim(clipId: string, newEnd: TimelineFrame): {
|
|
1082
|
+
accepted: boolean;
|
|
1083
|
+
errors?: {
|
|
1084
|
+
code: string;
|
|
1085
|
+
message: string;
|
|
1086
|
+
}[];
|
|
1087
|
+
};
|
|
1088
|
+
/**
|
|
1089
|
+
* Insert edit - insert clip and shift subsequent clips right
|
|
1090
|
+
*
|
|
1091
|
+
* @param trackId - ID of the track to insert into
|
|
1092
|
+
* @param clip - Clip to insert
|
|
1093
|
+
* @param atFrame - Frame to insert at
|
|
1094
|
+
* @returns Dispatch result
|
|
1095
|
+
*/
|
|
1096
|
+
insertEdit(trackId: string, clip: Clip, atFrame: TimelineFrame): {
|
|
1097
|
+
accepted: boolean;
|
|
1098
|
+
errors?: {
|
|
1099
|
+
code: string;
|
|
1100
|
+
message: string;
|
|
1101
|
+
}[];
|
|
1102
|
+
};
|
|
1103
|
+
/**
|
|
1104
|
+
* Ripple move - move clip and shift surrounding clips to accommodate
|
|
1105
|
+
*
|
|
1106
|
+
* This moves a clip to a new position while maintaining timeline continuity:
|
|
1107
|
+
* - Closes the gap at the source position
|
|
1108
|
+
* - Makes space at the destination position
|
|
1109
|
+
* - All operations are atomic (single undo entry)
|
|
1110
|
+
*
|
|
1111
|
+
* @param clipId - ID of the clip to move
|
|
1112
|
+
* @param newStart - New start frame for the clip
|
|
1113
|
+
* @returns Dispatch result
|
|
1114
|
+
*/
|
|
1115
|
+
rippleMove(clipId: string, newStart: TimelineFrame): {
|
|
1116
|
+
accepted: boolean;
|
|
1117
|
+
errors?: {
|
|
1118
|
+
code: string;
|
|
1119
|
+
message: string;
|
|
1120
|
+
}[];
|
|
1121
|
+
};
|
|
1122
|
+
/**
|
|
1123
|
+
* Insert move - move clip and shift destination clips right
|
|
1124
|
+
*
|
|
1125
|
+
* This moves a clip to a new position without closing the gap at source:
|
|
1126
|
+
* - Leaves gap at the source position
|
|
1127
|
+
* - Pushes all clips at destination right to make space
|
|
1128
|
+
* - All operations are atomic (single undo entry)
|
|
1129
|
+
*
|
|
1130
|
+
* @param clipId - ID of the clip to move
|
|
1131
|
+
* @param newStart - New start frame for the clip
|
|
1132
|
+
* @returns Dispatch result
|
|
1133
|
+
*/
|
|
1134
|
+
insertMove(clipId: string, newStart: TimelineFrame): {
|
|
1135
|
+
accepted: boolean;
|
|
1136
|
+
errors?: {
|
|
1137
|
+
code: string;
|
|
1138
|
+
message: string;
|
|
1139
|
+
}[];
|
|
1140
|
+
};
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
/**
|
|
1144
|
+
* OPERATION PRIMITIVES — Phase 0 compliant
|
|
1145
|
+
*
|
|
1146
|
+
* The ONLY way to express a mutation in the engine.
|
|
1147
|
+
* All mutations flow through: OperationPrimitive[] → Transaction → Dispatcher.
|
|
1148
|
+
*
|
|
1149
|
+
* RULE: Never add a new mutation function.
|
|
1150
|
+
* Add a new type to OperationPrimitive, handle it in the Dispatcher switch,
|
|
1151
|
+
* update the InvariantChecker, and update OPERATIONS.md.
|
|
1152
|
+
*
|
|
1153
|
+
* RULE: Transactions are all-or-nothing.
|
|
1154
|
+
* If any primitive fails validation, the entire Transaction is rejected.
|
|
1155
|
+
*/
|
|
1156
|
+
|
|
1157
|
+
type OperationPrimitive = {
|
|
1158
|
+
type: 'MOVE_CLIP';
|
|
1159
|
+
clipId: ClipId;
|
|
1160
|
+
newTimelineStart: TimelineFrame;
|
|
1161
|
+
targetTrackId?: TrackId;
|
|
1162
|
+
} | {
|
|
1163
|
+
type: 'RESIZE_CLIP';
|
|
1164
|
+
clipId: ClipId;
|
|
1165
|
+
edge: 'start' | 'end';
|
|
1166
|
+
newFrame: TimelineFrame;
|
|
1167
|
+
} | {
|
|
1168
|
+
type: 'SLICE_CLIP';
|
|
1169
|
+
clipId: ClipId;
|
|
1170
|
+
atFrame: TimelineFrame;
|
|
1171
|
+
} | {
|
|
1172
|
+
type: 'DELETE_CLIP';
|
|
1173
|
+
clipId: ClipId;
|
|
1174
|
+
} | {
|
|
1175
|
+
type: 'INSERT_CLIP';
|
|
1176
|
+
clip: Clip;
|
|
1177
|
+
trackId: TrackId;
|
|
1178
|
+
} | {
|
|
1179
|
+
type: 'SET_MEDIA_BOUNDS';
|
|
1180
|
+
clipId: ClipId;
|
|
1181
|
+
mediaIn: TimelineFrame;
|
|
1182
|
+
mediaOut: TimelineFrame;
|
|
1183
|
+
} | {
|
|
1184
|
+
type: 'SET_CLIP_ENABLED';
|
|
1185
|
+
clipId: ClipId;
|
|
1186
|
+
enabled: boolean;
|
|
1187
|
+
} | {
|
|
1188
|
+
type: 'SET_CLIP_REVERSED';
|
|
1189
|
+
clipId: ClipId;
|
|
1190
|
+
reversed: boolean;
|
|
1191
|
+
} | {
|
|
1192
|
+
type: 'SET_CLIP_SPEED';
|
|
1193
|
+
clipId: ClipId;
|
|
1194
|
+
speed: number;
|
|
1195
|
+
} | {
|
|
1196
|
+
type: 'SET_CLIP_COLOR';
|
|
1197
|
+
clipId: ClipId;
|
|
1198
|
+
color: string | null;
|
|
1199
|
+
} | {
|
|
1200
|
+
type: 'SET_CLIP_NAME';
|
|
1201
|
+
clipId: ClipId;
|
|
1202
|
+
name: string | null;
|
|
1203
|
+
} | {
|
|
1204
|
+
type: 'ADD_TRACK';
|
|
1205
|
+
track: Track;
|
|
1206
|
+
} | {
|
|
1207
|
+
type: 'DELETE_TRACK';
|
|
1208
|
+
trackId: TrackId;
|
|
1209
|
+
} | {
|
|
1210
|
+
type: 'REORDER_TRACK';
|
|
1211
|
+
trackId: TrackId;
|
|
1212
|
+
newIndex: number;
|
|
1213
|
+
} | {
|
|
1214
|
+
type: 'SET_TRACK_HEIGHT';
|
|
1215
|
+
trackId: TrackId;
|
|
1216
|
+
height: number;
|
|
1217
|
+
} | {
|
|
1218
|
+
type: 'SET_TRACK_NAME';
|
|
1219
|
+
trackId: TrackId;
|
|
1220
|
+
name: string;
|
|
1221
|
+
} | {
|
|
1222
|
+
type: 'REGISTER_ASSET';
|
|
1223
|
+
asset: Asset;
|
|
1224
|
+
} | {
|
|
1225
|
+
type: 'UNREGISTER_ASSET';
|
|
1226
|
+
assetId: AssetId;
|
|
1227
|
+
} | {
|
|
1228
|
+
type: 'SET_ASSET_STATUS';
|
|
1229
|
+
assetId: AssetId;
|
|
1230
|
+
status: AssetStatus;
|
|
1231
|
+
} | {
|
|
1232
|
+
type: 'RENAME_TIMELINE';
|
|
1233
|
+
name: string;
|
|
1234
|
+
} | {
|
|
1235
|
+
type: 'SET_TIMELINE_DURATION';
|
|
1236
|
+
duration: TimelineFrame;
|
|
1237
|
+
} | {
|
|
1238
|
+
type: 'SET_TIMELINE_START_TC';
|
|
1239
|
+
startTimecode: Timecode;
|
|
1240
|
+
} | {
|
|
1241
|
+
type: 'SET_SEQUENCE_SETTINGS';
|
|
1242
|
+
settings: Partial<SequenceSettings>;
|
|
1243
|
+
} | {
|
|
1244
|
+
type: 'ADD_MARKER';
|
|
1245
|
+
marker: Marker;
|
|
1246
|
+
} | {
|
|
1247
|
+
type: 'MOVE_MARKER';
|
|
1248
|
+
markerId: MarkerId;
|
|
1249
|
+
newFrame: TimelineFrame;
|
|
1250
|
+
} | {
|
|
1251
|
+
type: 'DELETE_MARKER';
|
|
1252
|
+
markerId: MarkerId;
|
|
1253
|
+
} | {
|
|
1254
|
+
type: 'SET_IN_POINT';
|
|
1255
|
+
frame: TimelineFrame | null;
|
|
1256
|
+
} | {
|
|
1257
|
+
type: 'SET_OUT_POINT';
|
|
1258
|
+
frame: TimelineFrame | null;
|
|
1259
|
+
} | {
|
|
1260
|
+
type: 'ADD_BEAT_GRID';
|
|
1261
|
+
beatGrid: BeatGrid;
|
|
1262
|
+
} | {
|
|
1263
|
+
type: 'REMOVE_BEAT_GRID';
|
|
1264
|
+
} | {
|
|
1265
|
+
type: 'INSERT_GENERATOR';
|
|
1266
|
+
generator: Generator;
|
|
1267
|
+
trackId: TrackId;
|
|
1268
|
+
atFrame: TimelineFrame;
|
|
1269
|
+
} | {
|
|
1270
|
+
type: 'ADD_CAPTION';
|
|
1271
|
+
caption: Omit<Caption, 'style'> & {
|
|
1272
|
+
style?: CaptionStyle;
|
|
1273
|
+
};
|
|
1274
|
+
trackId: TrackId;
|
|
1275
|
+
} | {
|
|
1276
|
+
type: 'EDIT_CAPTION';
|
|
1277
|
+
captionId: CaptionId;
|
|
1278
|
+
trackId: TrackId;
|
|
1279
|
+
text?: string;
|
|
1280
|
+
language?: string;
|
|
1281
|
+
style?: Partial<CaptionStyle>;
|
|
1282
|
+
burnIn?: boolean;
|
|
1283
|
+
startFrame?: TimelineFrame;
|
|
1284
|
+
endFrame?: TimelineFrame;
|
|
1285
|
+
} | {
|
|
1286
|
+
type: 'DELETE_CAPTION';
|
|
1287
|
+
captionId: CaptionId;
|
|
1288
|
+
trackId: TrackId;
|
|
1289
|
+
} | {
|
|
1290
|
+
type: 'ADD_EFFECT';
|
|
1291
|
+
clipId: ClipId;
|
|
1292
|
+
effect: Effect;
|
|
1293
|
+
} | {
|
|
1294
|
+
type: 'REMOVE_EFFECT';
|
|
1295
|
+
clipId: ClipId;
|
|
1296
|
+
effectId: EffectId;
|
|
1297
|
+
} | {
|
|
1298
|
+
type: 'REORDER_EFFECT';
|
|
1299
|
+
clipId: ClipId;
|
|
1300
|
+
effectId: EffectId;
|
|
1301
|
+
newIndex: number;
|
|
1302
|
+
} | {
|
|
1303
|
+
type: 'SET_EFFECT_ENABLED';
|
|
1304
|
+
clipId: ClipId;
|
|
1305
|
+
effectId: EffectId;
|
|
1306
|
+
enabled: boolean;
|
|
1307
|
+
} | {
|
|
1308
|
+
type: 'SET_EFFECT_PARAM';
|
|
1309
|
+
clipId: ClipId;
|
|
1310
|
+
effectId: EffectId;
|
|
1311
|
+
key: string;
|
|
1312
|
+
value: number | string | boolean;
|
|
1313
|
+
} | {
|
|
1314
|
+
type: 'ADD_KEYFRAME';
|
|
1315
|
+
clipId: ClipId;
|
|
1316
|
+
effectId: EffectId;
|
|
1317
|
+
keyframe: Keyframe;
|
|
1318
|
+
} | {
|
|
1319
|
+
type: 'MOVE_KEYFRAME';
|
|
1320
|
+
clipId: ClipId;
|
|
1321
|
+
effectId: EffectId;
|
|
1322
|
+
keyframeId: KeyframeId;
|
|
1323
|
+
newFrame: TimelineFrame;
|
|
1324
|
+
} | {
|
|
1325
|
+
type: 'DELETE_KEYFRAME';
|
|
1326
|
+
clipId: ClipId;
|
|
1327
|
+
effectId: EffectId;
|
|
1328
|
+
keyframeId: KeyframeId;
|
|
1329
|
+
} | {
|
|
1330
|
+
type: 'SET_KEYFRAME_EASING';
|
|
1331
|
+
clipId: ClipId;
|
|
1332
|
+
effectId: EffectId;
|
|
1333
|
+
keyframeId: KeyframeId;
|
|
1334
|
+
easing: EasingCurve;
|
|
1335
|
+
} | {
|
|
1336
|
+
type: 'SET_CLIP_TRANSFORM';
|
|
1337
|
+
clipId: ClipId;
|
|
1338
|
+
transform: Partial<ClipTransform>;
|
|
1339
|
+
} | {
|
|
1340
|
+
type: 'SET_AUDIO_PROPERTIES';
|
|
1341
|
+
clipId: ClipId;
|
|
1342
|
+
properties: Partial<AudioProperties>;
|
|
1343
|
+
} | {
|
|
1344
|
+
type: 'ADD_TRANSITION';
|
|
1345
|
+
clipId: ClipId;
|
|
1346
|
+
transition: Transition;
|
|
1347
|
+
} | {
|
|
1348
|
+
type: 'DELETE_TRANSITION';
|
|
1349
|
+
clipId: ClipId;
|
|
1350
|
+
} | {
|
|
1351
|
+
type: 'SET_TRANSITION_DURATION';
|
|
1352
|
+
clipId: ClipId;
|
|
1353
|
+
durationFrames: number;
|
|
1354
|
+
} | {
|
|
1355
|
+
type: 'SET_TRANSITION_ALIGNMENT';
|
|
1356
|
+
clipId: ClipId;
|
|
1357
|
+
alignment: TransitionAlignment;
|
|
1358
|
+
} | {
|
|
1359
|
+
type: 'LINK_CLIPS';
|
|
1360
|
+
linkGroup: LinkGroup;
|
|
1361
|
+
} | {
|
|
1362
|
+
type: 'UNLINK_CLIPS';
|
|
1363
|
+
linkGroupId: LinkGroupId;
|
|
1364
|
+
} | {
|
|
1365
|
+
type: 'ADD_TRACK_GROUP';
|
|
1366
|
+
trackGroup: TrackGroup;
|
|
1367
|
+
} | {
|
|
1368
|
+
type: 'DELETE_TRACK_GROUP';
|
|
1369
|
+
trackGroupId: TrackGroupId;
|
|
1370
|
+
} | {
|
|
1371
|
+
type: 'SET_TRACK_BLEND_MODE';
|
|
1372
|
+
trackId: TrackId;
|
|
1373
|
+
blendMode: string;
|
|
1374
|
+
} | {
|
|
1375
|
+
type: 'SET_TRACK_OPACITY';
|
|
1376
|
+
trackId: TrackId;
|
|
1377
|
+
opacity: number;
|
|
1378
|
+
};
|
|
1379
|
+
/**
|
|
1380
|
+
* Transaction — an atomic, labeled batch of OperationPrimitives.
|
|
1381
|
+
*
|
|
1382
|
+
* All primitives in a Transaction are validated before any are applied.
|
|
1383
|
+
* If one fails, none are applied. This is the all-or-nothing rule.
|
|
1384
|
+
*/
|
|
1385
|
+
type Transaction = {
|
|
1386
|
+
readonly id: string;
|
|
1387
|
+
readonly label: string;
|
|
1388
|
+
readonly timestamp: number;
|
|
1389
|
+
readonly operations: readonly OperationPrimitive[];
|
|
1390
|
+
};
|
|
1391
|
+
type RejectionReason = 'OVERLAP' | 'LOCKED_TRACK' | 'ASSET_MISSING' | 'TYPE_MISMATCH' | 'OUT_OF_BOUNDS' | 'MEDIA_BOUNDS_INVALID' | 'ASSET_IN_USE' | 'TRACK_NOT_EMPTY' | 'SPEED_INVALID' | 'INVARIANT_VIOLATED' | 'NOT_FOUND' | 'BEAT_GRID_EXISTS' | 'CLIP_NOT_FOUND' | 'DUPLICATE_EFFECT_ID' | 'EFFECT_NOT_FOUND' | 'EFFECT_INDEX_OUT_OF_RANGE' | 'KEYFRAME_NOT_FOUND' | 'DUPLICATE_KEYFRAME_ID' | 'INVALID_RANGE' | 'TRANSITION_NOT_FOUND' | 'LINK_GROUP_NOT_FOUND' | 'TRACK_GROUP_NOT_FOUND' | 'DUPLICATE_LINK_GROUP_ID' | 'DUPLICATE_TRACK_GROUP_ID' | 'INVALID_OPACITY' | 'TRACK_NOT_FOUND';
|
|
1392
|
+
type DispatchResult = {
|
|
1393
|
+
accepted: true;
|
|
1394
|
+
nextState: TimelineState;
|
|
1395
|
+
} | {
|
|
1396
|
+
accepted: false;
|
|
1397
|
+
reason: RejectionReason;
|
|
1398
|
+
message: string;
|
|
1399
|
+
};
|
|
1400
|
+
type ViolationType = 'OVERLAP' | 'MEDIA_BOUNDS_INVALID' | 'ASSET_MISSING' | 'TRACK_TYPE_MISMATCH' | 'CLIP_BEYOND_TIMELINE' | 'TRACK_NOT_SORTED' | 'DURATION_MISMATCH' | 'SPEED_INVALID' | 'SCHEMA_VERSION_MISMATCH' | 'MARKER_OUT_OF_BOUNDS' | 'IN_OUT_INVALID' | 'BEAT_GRID_INVALID' | 'CAPTION_OUT_OF_BOUNDS' | 'CAPTION_OVERLAP' | 'EFFECT_NOT_FOUND' | 'KEYFRAME_NOT_FOUND' | 'KEYFRAME_ORDER_VIOLATION' | 'EFFECT_INDEX_OUT_OF_RANGE' | 'INVALID_RENDER_STAGE' | 'TRACK_GROUP_NOT_FOUND' | 'INVALID_OPACITY' | 'INVALID_RANGE' | 'LINK_GROUP_NOT_FOUND';
|
|
1401
|
+
type InvariantViolation = {
|
|
1402
|
+
readonly type: ViolationType;
|
|
1403
|
+
readonly entityId: string;
|
|
1404
|
+
readonly message: string;
|
|
1405
|
+
};
|
|
1406
|
+
|
|
1407
|
+
/**
|
|
1408
|
+
* DISPATCHER — Phase 0 compliant
|
|
1409
|
+
*
|
|
1410
|
+
* The ONLY entry point for mutating TimelineState.
|
|
1411
|
+
* Validates first, applies atomically, checks invariants.
|
|
1412
|
+
*
|
|
1413
|
+
* Algorithm:
|
|
1414
|
+
* 1. For each operation: run per-primitive validator → reject immediately on failure
|
|
1415
|
+
* 2. Apply all operations sequentially to get proposedState
|
|
1416
|
+
* 3. Run checkInvariants(proposedState) → reject on any violation
|
|
1417
|
+
* 4. Bump timeline.version by 1 and return accepted
|
|
1418
|
+
*
|
|
1419
|
+
* RULE: If one primitive fails, zero primitives are applied.
|
|
1420
|
+
*/
|
|
1421
|
+
|
|
1422
|
+
declare function dispatch(state: TimelineState, transaction: Transaction): DispatchResult;
|
|
1423
|
+
|
|
1424
|
+
/**
|
|
1425
|
+
* INVARIANT CHECKER — Phase 0 compliant
|
|
1426
|
+
*
|
|
1427
|
+
* The most critical file in the engine.
|
|
1428
|
+
* checkInvariants() runs after every proposed state change inside the Dispatcher.
|
|
1429
|
+
* Zero violations is the only acceptable result in tests and at commit time.
|
|
1430
|
+
*
|
|
1431
|
+
* RULE: Run checkInvariants in EVERY test after every state mutation.
|
|
1432
|
+
*/
|
|
1433
|
+
|
|
1434
|
+
declare function checkInvariants(state: TimelineState): InvariantViolation[];
|
|
1435
|
+
|
|
1436
|
+
/**
|
|
1437
|
+
* Transaction compression policy — Phase 7 Step 3
|
|
1438
|
+
*
|
|
1439
|
+
* Rapid same-type ops within a time window can be merged
|
|
1440
|
+
* into a single history entry (last-write-wins).
|
|
1441
|
+
*/
|
|
1442
|
+
type CompressionPolicy = {
|
|
1443
|
+
readonly kind: 'none';
|
|
1444
|
+
} | {
|
|
1445
|
+
readonly kind: 'last-write-wins';
|
|
1446
|
+
readonly windowMs: number;
|
|
1447
|
+
};
|
|
1448
|
+
type CompressibleOpType = 'MOVE_CLIP' | 'SET_CLIP_TRANSFORM' | 'SET_AUDIO_PROPERTIES' | 'SET_EFFECT_PARAM' | 'MOVE_KEYFRAME' | 'SET_TRANSITION_DURATION' | 'MOVE_MARKER' | 'SET_IN_POINT' | 'SET_OUT_POINT' | 'SET_TRACK_OPACITY';
|
|
1449
|
+
declare const DEFAULT_COMPRESSION_POLICY: CompressionPolicy;
|
|
1450
|
+
declare const NO_COMPRESSION: CompressionPolicy;
|
|
1451
|
+
|
|
1452
|
+
/**
|
|
1453
|
+
* HISTORY ENGINE
|
|
1454
|
+
*
|
|
1455
|
+
* Snapshot-based undo/redo system for timeline state.
|
|
1456
|
+
*
|
|
1457
|
+
* Two APIs:
|
|
1458
|
+
* - HistoryState + pure functions (createHistory, pushHistory, undo, redo)
|
|
1459
|
+
* - HistoryStack class with compression, checkpoints, and persistence
|
|
1460
|
+
*/
|
|
1461
|
+
|
|
1462
|
+
/**
|
|
1463
|
+
* HistoryState - The history container
|
|
1464
|
+
*
|
|
1465
|
+
* Contains:
|
|
1466
|
+
* - past: Array of previous states (oldest first)
|
|
1467
|
+
* - present: Current state
|
|
1468
|
+
* - future: Array of states that can be redone (newest first)
|
|
1469
|
+
* - limit: Maximum number of past states to keep
|
|
1470
|
+
*/
|
|
1471
|
+
interface HistoryState {
|
|
1472
|
+
past: TimelineState[];
|
|
1473
|
+
present: TimelineState;
|
|
1474
|
+
future: TimelineState[];
|
|
1475
|
+
limit: number;
|
|
1476
|
+
}
|
|
1477
|
+
/**
|
|
1478
|
+
* Create a new history state
|
|
1479
|
+
*
|
|
1480
|
+
* @param initialState - Initial timeline state
|
|
1481
|
+
* @param limit - Maximum number of past states to keep (default: 50)
|
|
1482
|
+
* @returns A new HistoryState
|
|
1483
|
+
*/
|
|
1484
|
+
declare function createHistory(initialState: TimelineState, limit?: number): HistoryState;
|
|
1485
|
+
/**
|
|
1486
|
+
* Push a new state to history
|
|
1487
|
+
*
|
|
1488
|
+
* Moves current state to past, sets new state as present,
|
|
1489
|
+
* and clears future (can't redo after new action).
|
|
1490
|
+
*
|
|
1491
|
+
* @param history - Current history state
|
|
1492
|
+
* @param newState - New timeline state to push
|
|
1493
|
+
* @returns New history state with new state pushed
|
|
1494
|
+
*/
|
|
1495
|
+
declare function pushHistory(history: HistoryState, newState: TimelineState): HistoryState;
|
|
1496
|
+
/**
|
|
1497
|
+
* Undo the last action
|
|
1498
|
+
*
|
|
1499
|
+
* Moves current state to future, pops last state from past
|
|
1500
|
+
* and sets it as present.
|
|
1501
|
+
*
|
|
1502
|
+
* @param history - Current history state
|
|
1503
|
+
* @returns New history state with undo applied
|
|
1504
|
+
*/
|
|
1505
|
+
declare function undo(history: HistoryState): HistoryState;
|
|
1506
|
+
/**
|
|
1507
|
+
* Redo the last undone action
|
|
1508
|
+
*
|
|
1509
|
+
* Moves current state to past, pops first state from future
|
|
1510
|
+
* and sets it as present.
|
|
1511
|
+
*
|
|
1512
|
+
* @param history - Current history state
|
|
1513
|
+
* @returns New history state with redo applied
|
|
1514
|
+
*/
|
|
1515
|
+
declare function redo(history: HistoryState): HistoryState;
|
|
1516
|
+
/**
|
|
1517
|
+
* Check if undo is available
|
|
1518
|
+
*
|
|
1519
|
+
* @param history - Current history state
|
|
1520
|
+
* @returns true if undo is available
|
|
1521
|
+
*/
|
|
1522
|
+
declare function canUndo(history: HistoryState): boolean;
|
|
1523
|
+
/**
|
|
1524
|
+
* Check if redo is available
|
|
1525
|
+
*
|
|
1526
|
+
* @param history - Current history state
|
|
1527
|
+
* @returns true if redo is available
|
|
1528
|
+
*/
|
|
1529
|
+
declare function canRedo(history: HistoryState): boolean;
|
|
1530
|
+
/**
|
|
1531
|
+
* Get the current state from history
|
|
1532
|
+
*
|
|
1533
|
+
* @param history - Current history state
|
|
1534
|
+
* @returns The current timeline state
|
|
1535
|
+
*/
|
|
1536
|
+
declare function getCurrentState(history: HistoryState): TimelineState;
|
|
1537
|
+
type HistoryEntry = {
|
|
1538
|
+
readonly state: TimelineState;
|
|
1539
|
+
readonly transaction: Transaction;
|
|
1540
|
+
};
|
|
1541
|
+
declare class HistoryStack {
|
|
1542
|
+
private entries;
|
|
1543
|
+
private undoIndex;
|
|
1544
|
+
private limit;
|
|
1545
|
+
private compressor;
|
|
1546
|
+
private clock;
|
|
1547
|
+
private checkpoints;
|
|
1548
|
+
constructor(limit?: number, policy?: CompressionPolicy, clock?: () => number);
|
|
1549
|
+
push(entry: HistoryEntry): void;
|
|
1550
|
+
pushWithCompression(entry: HistoryEntry, transaction: Transaction): void;
|
|
1551
|
+
resetCompression(): void;
|
|
1552
|
+
undo(): TimelineState | null;
|
|
1553
|
+
redo(): TimelineState | null;
|
|
1554
|
+
getCurrentState(): TimelineState | null;
|
|
1555
|
+
canUndo(): boolean;
|
|
1556
|
+
canRedo(): boolean;
|
|
1557
|
+
saveCheckpoint(name: string): void;
|
|
1558
|
+
restoreCheckpoint(name: string): HistoryEntry | null;
|
|
1559
|
+
listCheckpoints(): string[];
|
|
1560
|
+
clearCheckpoint(name: string): void;
|
|
1561
|
+
serialize(): string;
|
|
1562
|
+
static deserialize(raw: string, limit?: number, policy?: CompressionPolicy, clock?: () => number): HistoryStack;
|
|
1563
|
+
softLimitWarning(): boolean;
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
/**
|
|
1567
|
+
* TransactionCompressor — Phase 7 Step 3
|
|
1568
|
+
*
|
|
1569
|
+
* Decides whether a transaction should be merged into the previous
|
|
1570
|
+
* history entry (same op type within window).
|
|
1571
|
+
*/
|
|
1572
|
+
|
|
1573
|
+
declare class TransactionCompressor {
|
|
1574
|
+
private lastOpType;
|
|
1575
|
+
private lastTime;
|
|
1576
|
+
private policy;
|
|
1577
|
+
private clock;
|
|
1578
|
+
constructor(policy?: CompressionPolicy, clock?: () => number);
|
|
1579
|
+
shouldCompress(transaction: Transaction, now: number): boolean;
|
|
1580
|
+
record(transaction: Transaction, now: number): void;
|
|
1581
|
+
reset(): void;
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
/**
|
|
1585
|
+
* SNAP INDEX — Phase 1
|
|
1586
|
+
*
|
|
1587
|
+
* Pure functions. Zero React/DOM imports. Zero mutation.
|
|
1588
|
+
*
|
|
1589
|
+
* Phase 1 snap sources: ClipStart, ClipEnd, Playhead.
|
|
1590
|
+
* Phase 2 will add: Marker, InPoint, OutPoint.
|
|
1591
|
+
* Phase 3 will add: BeatGrid.
|
|
1592
|
+
*
|
|
1593
|
+
* Priority table (do not change values):
|
|
1594
|
+
* Marker: 100
|
|
1595
|
+
* InPoint: 90
|
|
1596
|
+
* OutPoint: 90
|
|
1597
|
+
* ClipStart: 80
|
|
1598
|
+
* ClipEnd: 80
|
|
1599
|
+
* Playhead: 70
|
|
1600
|
+
* BeatGrid: 50
|
|
1601
|
+
*/
|
|
1602
|
+
|
|
1603
|
+
/**
|
|
1604
|
+
* All snap point sources across phases.
|
|
1605
|
+
* Defined in full now so SnapPoint & allowedTypes filters are stable.
|
|
1606
|
+
*/
|
|
1607
|
+
type SnapPointType = 'ClipStart' | 'ClipEnd' | 'Playhead' | 'Marker' | 'InPoint' | 'OutPoint' | 'BeatGrid';
|
|
1608
|
+
type SnapPoint = {
|
|
1609
|
+
readonly frame: TimelineFrame;
|
|
1610
|
+
readonly type: SnapPointType;
|
|
1611
|
+
readonly priority: number;
|
|
1612
|
+
readonly trackId: TrackId | null;
|
|
1613
|
+
readonly sourceId: string;
|
|
1614
|
+
};
|
|
1615
|
+
type SnapIndex = {
|
|
1616
|
+
readonly points: readonly SnapPoint[];
|
|
1617
|
+
readonly builtAt: number;
|
|
1618
|
+
readonly enabled: boolean;
|
|
1619
|
+
};
|
|
1620
|
+
/**
|
|
1621
|
+
* Build a SnapIndex from committed state + playhead position.
|
|
1622
|
+
*
|
|
1623
|
+
* RULE: Call via queueMicrotask after accepted dispatch.
|
|
1624
|
+
* Never call during a drag (pointer move).
|
|
1625
|
+
*
|
|
1626
|
+
* Phase 1 sources pulled (in order):
|
|
1627
|
+
* 1. ClipStart + ClipEnd from every clip on every track
|
|
1628
|
+
* 2. Playhead position (trackId = null)
|
|
1629
|
+
*/
|
|
1630
|
+
declare function buildSnapIndex(state: TimelineState, playheadFrame: TimelineFrame, enabled?: boolean): SnapIndex;
|
|
1631
|
+
/**
|
|
1632
|
+
* Find the highest-priority snap candidate within radiusFrames.
|
|
1633
|
+
*
|
|
1634
|
+
* Returns null when:
|
|
1635
|
+
* - index.enabled is false
|
|
1636
|
+
* - no point is within radiusFrames of frame
|
|
1637
|
+
*
|
|
1638
|
+
* Tiebreak (equidistant candidates): highest priority wins.
|
|
1639
|
+
* Second tiebreak (equal priority): first in sorted order.
|
|
1640
|
+
*
|
|
1641
|
+
* @param exclude sourceIds to skip (e.g. the clip being dragged)
|
|
1642
|
+
* @param allowedTypes if provided, only consider points of these types
|
|
1643
|
+
*/
|
|
1644
|
+
declare function nearest(index: SnapIndex, frame: TimelineFrame, radiusFrames: number, exclude?: readonly string[], allowedTypes?: readonly SnapPointType[]): SnapPoint | null;
|
|
1645
|
+
/**
|
|
1646
|
+
* Return a new SnapIndex with enabled toggled.
|
|
1647
|
+
* Does NOT rebuild points — pure field update.
|
|
1648
|
+
*/
|
|
1649
|
+
declare function toggleSnap(index: SnapIndex, enabled: boolean): SnapIndex;
|
|
1650
|
+
|
|
1651
|
+
/**
|
|
1652
|
+
* SnapIndexManager — Phase 7 Step 2
|
|
1653
|
+
*
|
|
1654
|
+
* Debounces SnapIndex rebuilds using queueMicrotask.
|
|
1655
|
+
* Multiple scheduleRebuild() calls in one turn → single rebuild.
|
|
1656
|
+
*/
|
|
1657
|
+
|
|
1658
|
+
declare class SnapIndexManager {
|
|
1659
|
+
private index;
|
|
1660
|
+
private state;
|
|
1661
|
+
private pending;
|
|
1662
|
+
getIndex(): SnapIndex | null;
|
|
1663
|
+
scheduleRebuild(state: TimelineState): void;
|
|
1664
|
+
rebuildSync(state: TimelineState): void;
|
|
1665
|
+
get isPending(): boolean;
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
/**
|
|
1669
|
+
* TOOL CONTRACT TYPES — Phase 1
|
|
1670
|
+
*
|
|
1671
|
+
* Zero implementation. Zero imports from React or DOM.
|
|
1672
|
+
* Every ITool must satisfy this interface exactly.
|
|
1673
|
+
*
|
|
1674
|
+
* RULES (from ITOOL_CONTRACT.md):
|
|
1675
|
+
* - onPointerMove NEVER calls dispatch
|
|
1676
|
+
* - onPointerUp NEVER mutates instance state
|
|
1677
|
+
* - onKeyDown, onKeyUp, onCancel are REQUIRED — implement as no-ops if unused
|
|
1678
|
+
*/
|
|
1679
|
+
|
|
1680
|
+
type ToolId = string & {
|
|
1681
|
+
readonly __brand: 'ToolId';
|
|
1682
|
+
};
|
|
1683
|
+
declare function toToolId(s: string): ToolId;
|
|
1684
|
+
/** Keyboard modifier state — available on ToolContext so getCursor() can
|
|
1685
|
+
* react to held keys even when no pointer event is firing. */
|
|
1686
|
+
type Modifiers = {
|
|
1687
|
+
readonly shift: boolean;
|
|
1688
|
+
readonly alt: boolean;
|
|
1689
|
+
readonly ctrl: boolean;
|
|
1690
|
+
readonly meta: boolean;
|
|
1691
|
+
};
|
|
1692
|
+
/** Normalised pointer event in frame-space.
|
|
1693
|
+
* ToolRouter populates clipId via hit-test — tools never recompute it. */
|
|
1694
|
+
type TimelinePointerEvent = {
|
|
1695
|
+
readonly frame: TimelineFrame;
|
|
1696
|
+
readonly trackId: TrackId | null;
|
|
1697
|
+
readonly clipId: ClipId | null;
|
|
1698
|
+
readonly x: number;
|
|
1699
|
+
readonly y: number;
|
|
1700
|
+
readonly buttons: number;
|
|
1701
|
+
readonly shiftKey: boolean;
|
|
1702
|
+
readonly altKey: boolean;
|
|
1703
|
+
readonly metaKey: boolean;
|
|
1704
|
+
};
|
|
1705
|
+
type TimelineKeyEvent = {
|
|
1706
|
+
readonly key: string;
|
|
1707
|
+
readonly code: string;
|
|
1708
|
+
readonly shiftKey: boolean;
|
|
1709
|
+
readonly altKey: boolean;
|
|
1710
|
+
readonly metaKey: boolean;
|
|
1711
|
+
readonly ctrlKey: boolean;
|
|
1712
|
+
/** True when key is held and OS is firing repeated keydowns. */
|
|
1713
|
+
readonly repeat?: boolean;
|
|
1714
|
+
};
|
|
1715
|
+
/** Pixel + frame region swept by a rubber-band (marquee) selection drag.
|
|
1716
|
+
* Populated by SelectionTool during rubber-band drags. */
|
|
1717
|
+
type RubberBandRegion = {
|
|
1718
|
+
readonly startFrame: TimelineFrame;
|
|
1719
|
+
readonly endFrame: TimelineFrame;
|
|
1720
|
+
readonly startY: number;
|
|
1721
|
+
readonly endY: number;
|
|
1722
|
+
};
|
|
1723
|
+
/** Ghost state produced by onPointerMove.
|
|
1724
|
+
* isProvisional: true is a compile-time discriminant so resolveClip()
|
|
1725
|
+
* can distinguish provisional from committed Clip[] arrays. */
|
|
1726
|
+
type ProvisionalState = {
|
|
1727
|
+
readonly clips: readonly Clip[];
|
|
1728
|
+
readonly rubberBand?: RubberBandRegion;
|
|
1729
|
+
readonly isProvisional: true;
|
|
1730
|
+
};
|
|
1731
|
+
/** Injected by TimelineEngine on every event call.
|
|
1732
|
+
* Tools never import TimelineEngine. They never call dispatch() directly. */
|
|
1733
|
+
type ToolContext = {
|
|
1734
|
+
readonly state: TimelineState;
|
|
1735
|
+
readonly snapIndex: SnapIndex;
|
|
1736
|
+
readonly pixelsPerFrame: number;
|
|
1737
|
+
/** Current modifier key state — updates on every pointer/key event. */
|
|
1738
|
+
readonly modifiers: Modifiers;
|
|
1739
|
+
/** Convert a client-pixel x-position to a TimelineFrame. */
|
|
1740
|
+
readonly frameAtX: (x: number) => TimelineFrame;
|
|
1741
|
+
/** Return the TrackId whose row contains client-pixel y, or null. */
|
|
1742
|
+
readonly trackAtY: (y: number) => TrackId | null;
|
|
1743
|
+
/** Query snap and return the snapped frame (or original if no hit).
|
|
1744
|
+
* Handles enabled/disabled, radius, exclusion, and type filter internally.
|
|
1745
|
+
* Tools never see radiusFrames or the enabled flag. */
|
|
1746
|
+
readonly snap: (frame: TimelineFrame, exclude?: readonly string[], allowedTypes?: readonly SnapPointType[]) => TimelineFrame;
|
|
1747
|
+
};
|
|
1748
|
+
interface ITool {
|
|
1749
|
+
readonly id: ToolId;
|
|
1750
|
+
/** Single-character keyboard shortcut, e.g. 'v', 'b', 'r'. Empty string = no shortcut. */
|
|
1751
|
+
readonly shortcutKey: string;
|
|
1752
|
+
/** Return the CSS cursor string for the current tool + modifier state.
|
|
1753
|
+
* Called on every pointermove — must be cheap. */
|
|
1754
|
+
getCursor(ctx: ToolContext): string;
|
|
1755
|
+
/** Return the SnapPointType categories this tool snaps to.
|
|
1756
|
+
* Used by ctx.snap() to filter the snap index automatically. */
|
|
1757
|
+
getSnapCandidateTypes(): readonly SnapPointType[];
|
|
1758
|
+
onPointerDown(event: TimelinePointerEvent, ctx: ToolContext): void;
|
|
1759
|
+
/** Return ProvisionalState for ghost rendering.
|
|
1760
|
+
* MUST NOT call dispatch. MUST NOT call engine methods. */
|
|
1761
|
+
onPointerMove(event: TimelinePointerEvent, ctx: ToolContext): ProvisionalState | null;
|
|
1762
|
+
/** Return a Transaction to commit, or null if this gesture produces no edit.
|
|
1763
|
+
* MUST NOT mutate any instance state. */
|
|
1764
|
+
onPointerUp(event: TimelinePointerEvent, ctx: ToolContext): Transaction | null;
|
|
1765
|
+
/** Handle a keydown — return a Transaction or null.
|
|
1766
|
+
* Required — implement as `return null` if unused. */
|
|
1767
|
+
onKeyDown(event: TimelineKeyEvent, ctx: ToolContext): Transaction | null;
|
|
1768
|
+
/** Handle a keyup — no return value.
|
|
1769
|
+
* Required — implement as no-op if unused. */
|
|
1770
|
+
onKeyUp(event: TimelineKeyEvent, ctx: ToolContext): void;
|
|
1771
|
+
/** Called when a gesture is interrupted (Escape, tool switch mid-drag).
|
|
1772
|
+
* Required — implement as no-op if unused. */
|
|
1773
|
+
onCancel(): void;
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
/**
|
|
1777
|
+
* TOOL REGISTRY — Phase 1
|
|
1778
|
+
*
|
|
1779
|
+
* Pure functions. No classes. No React. No state mutation.
|
|
1780
|
+
*
|
|
1781
|
+
* ToolRegistry is immutable data — activateTool returns a NEW registry.
|
|
1782
|
+
* The active tool lives here, not on TimelineEngine, keeping the engine thin.
|
|
1783
|
+
*
|
|
1784
|
+
* RULES:
|
|
1785
|
+
* - activateTool calls outgoing.onCancel() before switching
|
|
1786
|
+
* - activateTool throws on unknown id (programmer error, never user error)
|
|
1787
|
+
* - NoOpTool is the canonical do-nothing ITool (test double + startup default)
|
|
1788
|
+
*/
|
|
1789
|
+
|
|
1790
|
+
type ToolRegistry = {
|
|
1791
|
+
readonly tools: ReadonlyMap<ToolId, ITool>;
|
|
1792
|
+
readonly activeToolId: ToolId;
|
|
1793
|
+
};
|
|
1794
|
+
/**
|
|
1795
|
+
* Create an initial registry from an array of tools.
|
|
1796
|
+
*
|
|
1797
|
+
* @throws if defaultId is not present in the tools array
|
|
1798
|
+
*/
|
|
1799
|
+
declare function createRegistry(tools: readonly ITool[], defaultId: ToolId): ToolRegistry;
|
|
1800
|
+
/**
|
|
1801
|
+
* Activate a new tool.
|
|
1802
|
+
*
|
|
1803
|
+
* Steps (must run in order):
|
|
1804
|
+
* 1. Call outgoing tool's onCancel() — cleans up any in-progress drag state
|
|
1805
|
+
* 2. Validate that the new id exists in the registry
|
|
1806
|
+
* 3. Return a new ToolRegistry with activeToolId updated
|
|
1807
|
+
*
|
|
1808
|
+
* @throws if id is not registered
|
|
1809
|
+
*/
|
|
1810
|
+
declare function activateTool(registry: ToolRegistry, id: ToolId): ToolRegistry;
|
|
1811
|
+
/**
|
|
1812
|
+
* Return the currently active ITool.
|
|
1813
|
+
* Never returns undefined — registry invariant guarantees activeToolId is registered.
|
|
1814
|
+
*/
|
|
1815
|
+
declare function getActiveTool(registry: ToolRegistry): ITool;
|
|
1816
|
+
/**
|
|
1817
|
+
* Return a new registry with the tool added.
|
|
1818
|
+
* If a tool with the same id already exists, it is replaced.
|
|
1819
|
+
* activeToolId is unchanged.
|
|
1820
|
+
*/
|
|
1821
|
+
declare function registerTool(registry: ToolRegistry, tool: ITool): ToolRegistry;
|
|
1822
|
+
/**
|
|
1823
|
+
* Satisfies ITool with no side effects.
|
|
1824
|
+
*
|
|
1825
|
+
* Use for:
|
|
1826
|
+
* - Test doubles (spread and override only the methods you need)
|
|
1827
|
+
* - Default active tool on engine startup
|
|
1828
|
+
* - ToolRouter smoke tests
|
|
1829
|
+
*
|
|
1830
|
+
* onCancel() is a deliberate no-op: NoOpTool has no drag state to clean up.
|
|
1831
|
+
* Real tools will clear instance variables there.
|
|
1832
|
+
*/
|
|
1833
|
+
declare const NoOpTool: ITool;
|
|
1834
|
+
|
|
1835
|
+
/**
|
|
1836
|
+
* PROVISIONAL MANAGER — Phase 1
|
|
1837
|
+
*
|
|
1838
|
+
* Manages ghost state during pointer drags.
|
|
1839
|
+
*
|
|
1840
|
+
* RULES (from ITOOL_CONTRACT.md):
|
|
1841
|
+
* - setProvisional / clearProvisional return NEW objects — never mutate
|
|
1842
|
+
* - resolveClip checks provisional first, then committed state
|
|
1843
|
+
* - The engine calls clearProvisional() BEFORE dispatching onPointerUp's tx
|
|
1844
|
+
* - Provisional updates trigger notify() so ghosts render immediately
|
|
1845
|
+
*
|
|
1846
|
+
* resolveClip priority:
|
|
1847
|
+
* 1. provisional.clips has a clip with this id → return ghost version
|
|
1848
|
+
* 2. clip exists in committed state → return committed
|
|
1849
|
+
* 3. clip absent from both (deleted mid-drag) → return undefined
|
|
1850
|
+
*/
|
|
1851
|
+
|
|
1852
|
+
type ProvisionalManager = {
|
|
1853
|
+
readonly current: ProvisionalState | null;
|
|
1854
|
+
};
|
|
1855
|
+
/** Create an empty provisional manager (current = null). */
|
|
1856
|
+
declare function createProvisionalManager(): ProvisionalManager;
|
|
1857
|
+
/** Return a new manager with current set to state.
|
|
1858
|
+
* Pure — never mutates the original manager. */
|
|
1859
|
+
declare function setProvisional(_manager: ProvisionalManager, state: ProvisionalState): ProvisionalManager;
|
|
1860
|
+
/** Return a new manager with current set to null.
|
|
1861
|
+
* Pure — never mutates the original manager. */
|
|
1862
|
+
declare function clearProvisional(_manager: ProvisionalManager): ProvisionalManager;
|
|
1863
|
+
/**
|
|
1864
|
+
* Resolve which version of a clip to render.
|
|
1865
|
+
*
|
|
1866
|
+
* Priority:
|
|
1867
|
+
* 1. If manager.current has a clip with this id → return provisional (ghost)
|
|
1868
|
+
* 2. Otherwise → search committed state
|
|
1869
|
+
* 3. If absent from both (clip deleted mid-drag) → return undefined
|
|
1870
|
+
*
|
|
1871
|
+
* Returns undefined if the clip has been deleted from committed state
|
|
1872
|
+
* and is not in provisional. Components must handle this:
|
|
1873
|
+
* const clip = useClip(id)
|
|
1874
|
+
* if (!clip) return null ← required, not optional
|
|
1875
|
+
*
|
|
1876
|
+
* Call site in useClip selector:
|
|
1877
|
+
* () => resolveClip(id, engine.getSnapshot(), engine.getProvisionalManager())
|
|
1878
|
+
*/
|
|
1879
|
+
declare function resolveClip(clipId: ClipId, state: TimelineState, manager: ProvisionalManager): Clip | undefined;
|
|
1880
|
+
|
|
1881
|
+
/**
|
|
1882
|
+
* MARKER SEARCH API — Phase 3 Step 2
|
|
1883
|
+
*
|
|
1884
|
+
* Pure functions. Search state.timeline.markers only.
|
|
1885
|
+
*/
|
|
1886
|
+
|
|
1887
|
+
/**
|
|
1888
|
+
* Returns markers whose color exactly matches the given string.
|
|
1889
|
+
*/
|
|
1890
|
+
declare function findMarkersByColor(state: TimelineState, color: string): Marker[];
|
|
1891
|
+
/**
|
|
1892
|
+
* Returns markers whose label contains the given string (case-insensitive).
|
|
1893
|
+
*/
|
|
1894
|
+
declare function findMarkersByLabel(state: TimelineState, label: string): Marker[];
|
|
1895
|
+
|
|
1896
|
+
/**
|
|
1897
|
+
* SUBTITLE IMPORT — Phase 3 Step 3
|
|
1898
|
+
*
|
|
1899
|
+
* Pure functions for parsing SRT/VTT into Caption[].
|
|
1900
|
+
* No file IO. No DOM. No external deps.
|
|
1901
|
+
*/
|
|
1902
|
+
|
|
1903
|
+
declare const defaultCaptionStyle: CaptionStyle;
|
|
1904
|
+
type SRTParseOptions = {
|
|
1905
|
+
language?: string;
|
|
1906
|
+
burnIn?: boolean;
|
|
1907
|
+
defaultStyle?: Partial<CaptionStyle>;
|
|
1908
|
+
};
|
|
1909
|
+
type VTTParseOptions = SRTParseOptions;
|
|
1910
|
+
declare function parseSRT(raw: string, fps: number, options?: SRTParseOptions): Caption[];
|
|
1911
|
+
declare function parseVTT(raw: string, fps: number, options?: VTTParseOptions): Caption[];
|
|
1912
|
+
declare function subtitleImportToOps(captions: Caption[], trackId: TrackId): OperationPrimitive[];
|
|
1913
|
+
|
|
1914
|
+
/**
|
|
1915
|
+
* SelectionTool — Phase 2
|
|
1916
|
+
*
|
|
1917
|
+
* The most complex tool. Handles four interaction modes:
|
|
1918
|
+
* MODE 1: Single click → select/deselect clip (no drag)
|
|
1919
|
+
* MODE 2: Single drag → move one clip, produce MOVE_CLIP Transaction
|
|
1920
|
+
* MODE 3: Multi drag → move all selected clips by uniform delta, N× MOVE_CLIP
|
|
1921
|
+
* MODE 4: Rubber-band → marquee select clips, no Transaction
|
|
1922
|
+
*
|
|
1923
|
+
* SELECTION CONTRACT:
|
|
1924
|
+
* Selection lives on this instance as Set<ClipId>.
|
|
1925
|
+
* It is NOT in TimelineState. It is NOT undoable.
|
|
1926
|
+
* onCancel() resets all instance state, including selection.
|
|
1927
|
+
*
|
|
1928
|
+
* GHOST CLIP CONTRACT (corrected in design review):
|
|
1929
|
+
* Ghost clips are ALWAYS built by reading the live clip from ctx.state
|
|
1930
|
+
* then overriding position fields. Never spread a stored clip snapshot.
|
|
1931
|
+
* originalPositions is ONLY used in onPointerUp for MOVE_CLIP delta math.
|
|
1932
|
+
*
|
|
1933
|
+
* RULES:
|
|
1934
|
+
* - Zero imports from React, DOM, @timeline/react, @timeline/ui
|
|
1935
|
+
* - onPointerMove must never call dispatch
|
|
1936
|
+
* - onPointerUp must never mutate instance state
|
|
1937
|
+
* - Every instance variable appears in onCancel() — no exceptions
|
|
1938
|
+
*/
|
|
1939
|
+
|
|
1940
|
+
declare class SelectionTool implements ITool {
|
|
1941
|
+
readonly id: ToolId;
|
|
1942
|
+
readonly shortcutKey: string;
|
|
1943
|
+
private readonly selected;
|
|
1944
|
+
private mode;
|
|
1945
|
+
private dragStartFrame;
|
|
1946
|
+
private dragStartX;
|
|
1947
|
+
private dragStartY;
|
|
1948
|
+
private dragClipId;
|
|
1949
|
+
private isMultiDrag;
|
|
1950
|
+
/** Frame values only — no Clip objects. Used only in onPointerUp for delta math. */
|
|
1951
|
+
private originalPositions;
|
|
1952
|
+
private rubberBandStartFrame;
|
|
1953
|
+
private rubberBandStartY;
|
|
1954
|
+
private lastClientX;
|
|
1955
|
+
private lastHitEdge;
|
|
1956
|
+
getSelection(): ReadonlySet<ClipId>;
|
|
1957
|
+
clearSelection(): void;
|
|
1958
|
+
getCursor(_ctx: ToolContext): string;
|
|
1959
|
+
private _lastHoveredClipId;
|
|
1960
|
+
getSnapCandidateTypes(): readonly SnapPointType[];
|
|
1961
|
+
onPointerDown(event: TimelinePointerEvent, ctx: ToolContext): void;
|
|
1962
|
+
onPointerMove(event: TimelinePointerEvent, ctx: ToolContext): ProvisionalState | null;
|
|
1963
|
+
onPointerUp(event: TimelinePointerEvent, ctx: ToolContext): Transaction | null;
|
|
1964
|
+
onKeyDown(_event: TimelineKeyEvent, _ctx: ToolContext): Transaction | null;
|
|
1965
|
+
onKeyUp(_event: TimelineKeyEvent, _ctx: ToolContext): void;
|
|
1966
|
+
/**
|
|
1967
|
+
* Reset ALL instance state.
|
|
1968
|
+
* Every instance variable must appear here.
|
|
1969
|
+
* If a new variable is added to the class, it MUST be added here too.
|
|
1970
|
+
*/
|
|
1971
|
+
onCancel(): void;
|
|
1972
|
+
/** Reset per-gesture drag state WITHOUT clearing selection. */
|
|
1973
|
+
private _resetDragState;
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
/**
|
|
1977
|
+
* RazorTool — Phase 2 Step 2
|
|
1978
|
+
*
|
|
1979
|
+
* Click on a clip at any frame → split it into two clips at that frame.
|
|
1980
|
+
* Shift+click (ctx.modifiers.shift) → split ALL clips at that frame across ALL tracks.
|
|
1981
|
+
*
|
|
1982
|
+
* CONTRACT:
|
|
1983
|
+
* - No drag, no provisional ghost, no rubber-band
|
|
1984
|
+
* - onPointerMove always returns null
|
|
1985
|
+
* - Every Transaction is DELETE_CLIP + INSERT_CLIP(left) + INSERT_CLIP(right), per clip
|
|
1986
|
+
* - New ClipIds are generated by generateId() — replaceable in tests via _setIdGenerator()
|
|
1987
|
+
*
|
|
1988
|
+
* RULES:
|
|
1989
|
+
* - Zero imports from React, DOM, @timeline/react, @timeline/ui
|
|
1990
|
+
* - ctx.modifiers.shift is the authoritative source for shift detection (not event.shiftKey)
|
|
1991
|
+
* - Both halves need strictly positive duration — reject if atFrame is at clip boundary
|
|
1992
|
+
*/
|
|
1993
|
+
|
|
1994
|
+
declare class RazorTool implements ITool {
|
|
1995
|
+
readonly id: ToolId;
|
|
1996
|
+
readonly shortcutKey: string;
|
|
1997
|
+
/** Snapped slice frame captured at onPointerDown. */
|
|
1998
|
+
private pendingFrame;
|
|
1999
|
+
/** ClipId hit at onPointerDown. null if clicking empty space or for shift+all. */
|
|
2000
|
+
private pendingClipId;
|
|
2001
|
+
getCursor(_ctx: ToolContext): string;
|
|
2002
|
+
getSnapCandidateTypes(): readonly SnapPointType[];
|
|
2003
|
+
onPointerDown(event: TimelinePointerEvent, ctx: ToolContext): void;
|
|
2004
|
+
onPointerMove(_event: TimelinePointerEvent, _ctx: ToolContext): ProvisionalState | null;
|
|
2005
|
+
onPointerUp(event: TimelinePointerEvent, ctx: ToolContext): Transaction | null;
|
|
2006
|
+
onKeyDown(_event: TimelineKeyEvent, _ctx: ToolContext): Transaction | null;
|
|
2007
|
+
onKeyUp(_event: TimelineKeyEvent, _ctx: ToolContext): void;
|
|
2008
|
+
/**
|
|
2009
|
+
* Reset ALL instance state.
|
|
2010
|
+
* Every instance variable must appear here.
|
|
2011
|
+
*/
|
|
2012
|
+
onCancel(): void;
|
|
2013
|
+
private _findClip;
|
|
2014
|
+
/**
|
|
2015
|
+
* Slice every clip that contains `atFrame` across all tracks.
|
|
2016
|
+
* Groups operations per-clip: DELETE then INSERT left then INSERT right.
|
|
2017
|
+
* Tracks where no clip spans `atFrame` contribute zero operations.
|
|
2018
|
+
*/
|
|
2019
|
+
private _sliceAllTracks;
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2022
|
+
/**
|
|
2023
|
+
* RippleTrimTool — Phase 2 Step 3
|
|
2024
|
+
*
|
|
2025
|
+
* Drag a clip edge (start or end). The dragged edge moves.
|
|
2026
|
+
* All clips downstream of the edit point shift by the same delta.
|
|
2027
|
+
*
|
|
2028
|
+
* DOWNSTREAM DEFINITION:
|
|
2029
|
+
* END edge trim: clips with timelineStart >= original.timelineEnd (to the right)
|
|
2030
|
+
* START edge trim: clips with timelineEnd <= original.timelineStart (to the left)
|
|
2031
|
+
*
|
|
2032
|
+
* START EDGE SEMANTICS:
|
|
2033
|
+
* When the start edge moves right (+delta), left clips also shift right (+delta).
|
|
2034
|
+
* When the start edge moves left (-delta), left clips also shift left (-delta).
|
|
2035
|
+
* This is standard NLE ripple trim behavior (Premiere / Resolve convention).
|
|
2036
|
+
*
|
|
2037
|
+
* TRANSACTION ORDER:
|
|
2038
|
+
* RESIZE_CLIP first, then N× MOVE_CLIP.
|
|
2039
|
+
* Rolling-state validation means MOVE_CLIPs validate after RESIZE is applied.
|
|
2040
|
+
*
|
|
2041
|
+
* CLAMPING (applied before ghost and Transaction):
|
|
2042
|
+
* 1. Min duration: clip must remain ≥ 1 frame
|
|
2043
|
+
* 2. Media bounds: mediaIn must stay < mediaOut - 1 (START); mediaOut > mediaIn + 1 (END)
|
|
2044
|
+
* 3. Frame-0: for START trim, leftward shift must not push any left-clip below frame 0
|
|
2045
|
+
*
|
|
2046
|
+
* RULES:
|
|
2047
|
+
* - Zero imports from React, DOM, @timeline/react, @timeline/ui
|
|
2048
|
+
* - onPointerMove never dispatches
|
|
2049
|
+
* - onPointerUp never mutates instance state
|
|
2050
|
+
* - Every instance variable appears in onCancel()
|
|
2051
|
+
*/
|
|
2052
|
+
|
|
2053
|
+
declare class RippleTrimTool implements ITool {
|
|
2054
|
+
readonly id: ToolId;
|
|
2055
|
+
readonly shortcutKey: string;
|
|
2056
|
+
private dragClipId;
|
|
2057
|
+
private dragEdge;
|
|
2058
|
+
/** Original clip bounds — frame values only, never a stale Clip object. */
|
|
2059
|
+
private dragOrigStart;
|
|
2060
|
+
private dragOrigEnd;
|
|
2061
|
+
private dragOrigMediaIn;
|
|
2062
|
+
private dragOrigMediaOut;
|
|
2063
|
+
/**
|
|
2064
|
+
* Original positions of downstream clips.
|
|
2065
|
+
* Keyed by ClipId. Both timelineStart and timelineEnd stored — ghost needs
|
|
2066
|
+
* both to render, MOVE_CLIP only needs start (but duration is end - start).
|
|
2067
|
+
*/
|
|
2068
|
+
private originalDownstream;
|
|
2069
|
+
private lastHitEdge;
|
|
2070
|
+
private lastHoveredClipId;
|
|
2071
|
+
getCursor(_ctx: ToolContext): string;
|
|
2072
|
+
getSnapCandidateTypes(): readonly SnapPointType[];
|
|
2073
|
+
onPointerDown(event: TimelinePointerEvent, ctx: ToolContext): void;
|
|
2074
|
+
onPointerMove(event: TimelinePointerEvent, ctx: ToolContext): ProvisionalState | null;
|
|
2075
|
+
onPointerUp(event: TimelinePointerEvent, ctx: ToolContext): Transaction | null;
|
|
2076
|
+
onKeyDown(_event: TimelineKeyEvent, _ctx: ToolContext): Transaction | null;
|
|
2077
|
+
onKeyUp(_event: TimelineKeyEvent, _ctx: ToolContext): void;
|
|
2078
|
+
/** Reset ALL instance state. Every variable must appear here. */
|
|
2079
|
+
onCancel(): void;
|
|
2080
|
+
/**
|
|
2081
|
+
* Apply all clamping rules to the candidate newFrame.
|
|
2082
|
+
* Returns null if the resulting trim would produce a zero-or-negative-duration clip.
|
|
2083
|
+
*/
|
|
2084
|
+
private _clampFrame;
|
|
2085
|
+
/**
|
|
2086
|
+
* Build the ProvisionalState showing trimmed clip + all shifted downstream clips.
|
|
2087
|
+
* Always reads live clip data from ctx.state — never spreads stored clip objects.
|
|
2088
|
+
*/
|
|
2089
|
+
private _buildGhost;
|
|
2090
|
+
/** Reset per-drag instance state. Does NOT touch getCursor vars. */
|
|
2091
|
+
private _resetDragState;
|
|
2092
|
+
}
|
|
2093
|
+
|
|
2094
|
+
/**
|
|
2095
|
+
* RollTrimTool — Phase 2 Step 4
|
|
2096
|
+
*
|
|
2097
|
+
* Drag the boundary between two adjacent clips.
|
|
2098
|
+
* Left clip's end and right clip's start move together to the same frame.
|
|
2099
|
+
* Combined duration of both clips is unchanged.
|
|
2100
|
+
* No downstream ripple. No upstream ripple.
|
|
2101
|
+
*
|
|
2102
|
+
* TRANSACTION: 2× RESIZE_CLIP with identical newFrame. One history entry.
|
|
2103
|
+
*
|
|
2104
|
+
* CLAMP (precomputed at onPointerDown — only 5 instance vars needed):
|
|
2105
|
+
* minBoundary = max(leftOrig.timelineStart + 1,
|
|
2106
|
+
* origBoundary - (leftOrig.mediaOut - leftOrig.mediaIn - 1))
|
|
2107
|
+
* maxBoundary = min(rightOrig.timelineEnd - 1,
|
|
2108
|
+
* origBoundary + (rightOrig.mediaOut - rightOrig.mediaIn - 1))
|
|
2109
|
+
*
|
|
2110
|
+
* ORIGBOUNDARY AT onPointerUp:
|
|
2111
|
+
* Read from ctx.state (committed, not yet changed) — avoids a 6th instance var.
|
|
2112
|
+
*
|
|
2113
|
+
* RULES:
|
|
2114
|
+
* - Zero imports from React, DOM, @timeline/react, @timeline/ui
|
|
2115
|
+
* - onPointerMove never dispatches
|
|
2116
|
+
* - onPointerUp never mutates instance state
|
|
2117
|
+
* - Every instance variable appears in onCancel()
|
|
2118
|
+
* - Capture-before-reset pattern: compute clamp BEFORE _resetDragState()
|
|
2119
|
+
*/
|
|
2120
|
+
|
|
2121
|
+
declare class RollTrimTool implements ITool {
|
|
2122
|
+
readonly id: ToolId;
|
|
2123
|
+
readonly shortcutKey: string;
|
|
2124
|
+
private leftClipId;
|
|
2125
|
+
private rightClipId;
|
|
2126
|
+
/**
|
|
2127
|
+
* Precomputed clamp bounds — computed once at onPointerDown from all 4 constraints.
|
|
2128
|
+
* Avoids storing all 8 original clip bounds as separate instance vars.
|
|
2129
|
+
*/
|
|
2130
|
+
private minBoundary;
|
|
2131
|
+
private maxBoundary;
|
|
2132
|
+
/** True when the pointer is hovering a valid cut point (within EDGE_ZONE). */
|
|
2133
|
+
private isHoveringCut;
|
|
2134
|
+
getCursor(_ctx: ToolContext): string;
|
|
2135
|
+
getSnapCandidateTypes(): readonly SnapPointType[];
|
|
2136
|
+
onPointerDown(event: TimelinePointerEvent, ctx: ToolContext): void;
|
|
2137
|
+
onPointerMove(event: TimelinePointerEvent, ctx: ToolContext): ProvisionalState | null;
|
|
2138
|
+
onPointerUp(event: TimelinePointerEvent, ctx: ToolContext): Transaction | null;
|
|
2139
|
+
onKeyDown(_event: TimelineKeyEvent, _ctx: ToolContext): Transaction | null;
|
|
2140
|
+
onKeyUp(_event: TimelineKeyEvent, _ctx: ToolContext): void;
|
|
2141
|
+
/** Reset ALL instance state. Every variable must appear here. */
|
|
2142
|
+
onCancel(): void;
|
|
2143
|
+
private _buildGhost;
|
|
2144
|
+
private _resetDragState;
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2147
|
+
/**
|
|
2148
|
+
* SlipTool — Phase 2 Step 5
|
|
2149
|
+
*
|
|
2150
|
+
* Drag a clip to shift its media window. The clip's timeline position is
|
|
2151
|
+
* unchanged — only mediaIn and mediaOut move together by the same delta.
|
|
2152
|
+
*
|
|
2153
|
+
* OPERATION: Single SET_MEDIA_BOUNDS. No MOVE_CLIP. No RESIZE_CLIP. Nothing else.
|
|
2154
|
+
*
|
|
2155
|
+
* DELTA: rawDelta = event.frame - dragStartFrame (no snapping — slip is in media space)
|
|
2156
|
+
*
|
|
2157
|
+
* CLAMP:
|
|
2158
|
+
* minDelta = -clip.mediaIn → mediaIn + delta >= 0
|
|
2159
|
+
* maxDelta = asset.intrinsicDuration - clip.mediaOut → mediaOut + delta <= intrinsicDuration
|
|
2160
|
+
* clampedDelta = clamp(rawDelta, minDelta, maxDelta)
|
|
2161
|
+
*
|
|
2162
|
+
* SNAP: none. getSnapCandidateTypes returns [].
|
|
2163
|
+
*
|
|
2164
|
+
* RULES:
|
|
2165
|
+
* - Zero imports from React, DOM, @timeline/react, @timeline/ui
|
|
2166
|
+
* - onPointerMove never dispatches
|
|
2167
|
+
* - onPointerUp never mutates instance state
|
|
2168
|
+
* - Every instance variable appears in onCancel()
|
|
2169
|
+
* - Capture-before-reset: compute delta BEFORE _resetDragState()
|
|
2170
|
+
*/
|
|
2171
|
+
|
|
2172
|
+
declare class SlipTool implements ITool {
|
|
2173
|
+
readonly id: ToolId;
|
|
2174
|
+
readonly shortcutKey: string;
|
|
2175
|
+
/** Clip being slipped. Null when idle. */
|
|
2176
|
+
private dragClipId;
|
|
2177
|
+
/** Frame at which pointer went down. Delta = currentFrame - dragStartFrame. */
|
|
2178
|
+
private dragStartFrame;
|
|
2179
|
+
/** Staged by onPointerMove — getCursor() has no event parameter. */
|
|
2180
|
+
private isHoveringClip;
|
|
2181
|
+
getCursor(_ctx: ToolContext): string;
|
|
2182
|
+
getSnapCandidateTypes(): readonly SnapPointType[];
|
|
2183
|
+
onPointerDown(event: TimelinePointerEvent, _ctx: ToolContext): void;
|
|
2184
|
+
onPointerMove(event: TimelinePointerEvent, ctx: ToolContext): ProvisionalState | null;
|
|
2185
|
+
onPointerUp(event: TimelinePointerEvent, ctx: ToolContext): Transaction | null;
|
|
2186
|
+
onKeyDown(_event: TimelineKeyEvent, _ctx: ToolContext): Transaction | null;
|
|
2187
|
+
onKeyUp(_event: TimelineKeyEvent, _ctx: ToolContext): void;
|
|
2188
|
+
/** Reset ALL instance state. Every variable must appear here. */
|
|
2189
|
+
onCancel(): void;
|
|
2190
|
+
private _resetDragState;
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
/**
|
|
2194
|
+
* RippleDeleteTool — Phase 2 Step 6
|
|
2195
|
+
*
|
|
2196
|
+
* Click a clip to delete it. All clips to the right on the same track
|
|
2197
|
+
* shift left by the deleted clip's duration. No drag. No provisional state.
|
|
2198
|
+
*
|
|
2199
|
+
* TRANSACTION:
|
|
2200
|
+
* DELETE_CLIP { clipId }
|
|
2201
|
+
* MOVE_CLIP×N — one per downstream clip, sorted LEFT-TO-RIGHT
|
|
2202
|
+
*
|
|
2203
|
+
* MOVE_CLIP ordering rule (OPERATIONS.md: delta is negative → left-to-right):
|
|
2204
|
+
* Leftmost clip moves first into space vacated by DELETE_CLIP.
|
|
2205
|
+
* Each subsequent clip moves into space vacated by the one before it.
|
|
2206
|
+
* Wrong order → OVERLAP rejection from rolling-state validator.
|
|
2207
|
+
*
|
|
2208
|
+
* ACTIVATION: RippleDeleteTool is activated programmatically (e.g. when Delete
|
|
2209
|
+
* is pressed while a clip is selected in SelectionTool). shortcutKey is empty
|
|
2210
|
+
* because 'delete' is not a single-char tool-activation key.
|
|
2211
|
+
*
|
|
2212
|
+
* RULES:
|
|
2213
|
+
* - Zero imports from React, DOM, @timeline/react, @timeline/ui
|
|
2214
|
+
* - onPointerMove never dispatches (returns null always)
|
|
2215
|
+
* - onPointerUp never mutates instance state
|
|
2216
|
+
* - Every instance variable appears in onCancel()
|
|
2217
|
+
* - Capture-before-reset pattern applied in onPointerUp
|
|
2218
|
+
*/
|
|
2219
|
+
|
|
2220
|
+
declare class RippleDeleteTool implements ITool {
|
|
2221
|
+
readonly id: ToolId;
|
|
2222
|
+
readonly shortcutKey: string;
|
|
2223
|
+
/**
|
|
2224
|
+
* Clip targeted at onPointerDown. Read and cleared at onPointerUp.
|
|
2225
|
+
* No drag, no delta, no edge — this tool is click-only.
|
|
2226
|
+
*/
|
|
2227
|
+
private pendingClipId;
|
|
2228
|
+
/** Staged by onPointerMove — getCursor() has no event parameter. */
|
|
2229
|
+
private isHoveringClip;
|
|
2230
|
+
getCursor(_ctx: ToolContext): string;
|
|
2231
|
+
getSnapCandidateTypes(): readonly SnapPointType[];
|
|
2232
|
+
onPointerDown(event: TimelinePointerEvent, _ctx: ToolContext): void;
|
|
2233
|
+
onPointerMove(event: TimelinePointerEvent, _ctx: ToolContext): ProvisionalState | null;
|
|
2234
|
+
onPointerUp(_event: TimelinePointerEvent, ctx: ToolContext): Transaction | null;
|
|
2235
|
+
onKeyDown(_event: TimelineKeyEvent, _ctx: ToolContext): Transaction | null;
|
|
2236
|
+
onKeyUp(_event: TimelineKeyEvent, _ctx: ToolContext): void;
|
|
2237
|
+
/** Reset ALL instance state. Every variable must appear here. */
|
|
2238
|
+
onCancel(): void;
|
|
2239
|
+
private _resetState;
|
|
2240
|
+
}
|
|
2241
|
+
|
|
2242
|
+
/**
|
|
2243
|
+
* RippleInsertTool — Phase 2 Step 7
|
|
2244
|
+
*
|
|
2245
|
+
* Drag a clip from an asset and drop it onto a track.
|
|
2246
|
+
* Clips at or after the drop point shift RIGHT by insertDuration.
|
|
2247
|
+
* The inserted clip lands exactly at the drop point.
|
|
2248
|
+
*
|
|
2249
|
+
* TRANSACTION ORDER — critical:
|
|
2250
|
+
* MOVE_CLIPs first (RIGHT-TO-LEFT — +delta rule)
|
|
2251
|
+
* INSERT_CLIP last (gap is now open after all MOVE_CLIPs)
|
|
2252
|
+
*
|
|
2253
|
+
* INSTANCE VARIABLE GROUPS:
|
|
2254
|
+
* Group A (pending-insert): set by setPendingInsert(), preserved across drops,
|
|
2255
|
+
* cleared only by onCancel(). Cannot be changed mid-drag (guard in setPendingInsert).
|
|
2256
|
+
* Group B (drag-tracking): set by onPointerDown(), cleared by onPointerUp() and onCancel().
|
|
2257
|
+
*
|
|
2258
|
+
* PROVISIONAL STATE:
|
|
2259
|
+
* Ghost inserted clip (sentinel id 'provisional-insert') + all shifted right-clips.
|
|
2260
|
+
* Ghost id is NEVER written to committed state — real clip gets new id at onPointerUp.
|
|
2261
|
+
*
|
|
2262
|
+
* RULES:
|
|
2263
|
+
* - Zero imports from React, DOM, @timeline/react, @timeline/ui
|
|
2264
|
+
* - onPointerMove never dispatches
|
|
2265
|
+
* - onPointerUp never mutates instance state (capture-before-reset)
|
|
2266
|
+
* - Every instance variable appears in onCancel()
|
|
2267
|
+
*/
|
|
2268
|
+
|
|
2269
|
+
declare class RippleInsertTool implements ITool {
|
|
2270
|
+
readonly id: ToolId;
|
|
2271
|
+
readonly shortcutKey: string;
|
|
2272
|
+
private pendingAsset;
|
|
2273
|
+
private pendingMediaIn;
|
|
2274
|
+
private pendingMediaOut;
|
|
2275
|
+
private isDragging;
|
|
2276
|
+
/**
|
|
2277
|
+
* Configure what clip will be inserted on the next drag.
|
|
2278
|
+
* Preserves across drops — can be called once per asset, then drag many times.
|
|
2279
|
+
*
|
|
2280
|
+
* Guard: ignored if a drag is in progress — prevents ghost/Transaction mismatch
|
|
2281
|
+
* from async React state updates firing setPendingInsert mid-drag.
|
|
2282
|
+
*/
|
|
2283
|
+
setPendingInsert(asset: Asset, mediaIn: TimelineFrame, mediaOut: TimelineFrame): void;
|
|
2284
|
+
getCursor(_ctx: ToolContext): string;
|
|
2285
|
+
getSnapCandidateTypes(): readonly SnapPointType[];
|
|
2286
|
+
onPointerDown(event: TimelinePointerEvent, _ctx: ToolContext): void;
|
|
2287
|
+
onPointerMove(event: TimelinePointerEvent, ctx: ToolContext): ProvisionalState | null;
|
|
2288
|
+
onPointerUp(event: TimelinePointerEvent, ctx: ToolContext): Transaction | null;
|
|
2289
|
+
onKeyDown(_event: TimelineKeyEvent, _ctx: ToolContext): Transaction | null;
|
|
2290
|
+
onKeyUp(_event: TimelineKeyEvent, _ctx: ToolContext): void;
|
|
2291
|
+
/** Reset ALL instance state. Every variable must appear here. */
|
|
2292
|
+
onCancel(): void;
|
|
2293
|
+
/** Reset Group B only. Group A (pending-insert) is preserved for re-use. */
|
|
2294
|
+
private _resetDragState;
|
|
2295
|
+
}
|
|
2296
|
+
|
|
2297
|
+
/**
|
|
2298
|
+
* HandTool — Phase 2 Step 8
|
|
2299
|
+
*
|
|
2300
|
+
* Scroll/pan the timeline viewport by dragging.
|
|
2301
|
+
* This tool has ZERO effect on TimelineState.
|
|
2302
|
+
*
|
|
2303
|
+
* - Never produces a Transaction (onPointerUp returns null always)
|
|
2304
|
+
* - Never calls dispatch
|
|
2305
|
+
* - Never returns ProvisionalState (onPointerMove returns null always)
|
|
2306
|
+
* - Never creates ClipIds (_setIdGenerator not needed)
|
|
2307
|
+
*
|
|
2308
|
+
* THE SCROLL CALLBACK:
|
|
2309
|
+
* The UI registers a callback via setScrollCallback().
|
|
2310
|
+
* On every onPointerMove during drag, HandTool fires:
|
|
2311
|
+
* scrollCallback(event.x - lastX) ← pixel delta, not frame delta
|
|
2312
|
+
* The UI handles scrollLeft adjustment. HandTool has no DOM access.
|
|
2313
|
+
*
|
|
2314
|
+
* The callback is optional — drag tracking activates regardless.
|
|
2315
|
+
* If no callback is registered, delta is computed but discarded.
|
|
2316
|
+
* This allows testing drag tracking without a live callback.
|
|
2317
|
+
*
|
|
2318
|
+
* RULES:
|
|
2319
|
+
* - Zero imports from React, DOM, @timeline/react, @timeline/ui
|
|
2320
|
+
* - Every instance variable appears in onCancel()
|
|
2321
|
+
*/
|
|
2322
|
+
|
|
2323
|
+
declare class HandTool implements ITool {
|
|
2324
|
+
readonly id: ToolId;
|
|
2325
|
+
readonly shortcutKey: string;
|
|
2326
|
+
/**
|
|
2327
|
+
* Registered by the UI layer once at mount. Persists across drags and
|
|
2328
|
+
* cancels — not per-drag state. Pass null to unregister.
|
|
2329
|
+
*/
|
|
2330
|
+
private scrollCallback;
|
|
2331
|
+
/** Gates delta computation and cursor. */
|
|
2332
|
+
private isDragging;
|
|
2333
|
+
/**
|
|
2334
|
+
* X position (pixels) at the last pointer event.
|
|
2335
|
+
* Delta is event-to-event (not from start): deltaX = event.x - lastX.
|
|
2336
|
+
* Incremental delta is what UI scroll handlers expect (scrollLeft += deltaX).
|
|
2337
|
+
*/
|
|
2338
|
+
private lastX;
|
|
2339
|
+
/**
|
|
2340
|
+
* Register the UI's scroll handler. Called once at mount, not per-drag.
|
|
2341
|
+
* @param cb Receives pixel deltaX per move event. Pass null to unregister.
|
|
2342
|
+
*/
|
|
2343
|
+
setScrollCallback(cb: ((deltaX: number) => void) | null): void;
|
|
2344
|
+
getCursor(_ctx: ToolContext): string;
|
|
2345
|
+
getSnapCandidateTypes(): readonly SnapPointType[];
|
|
2346
|
+
onPointerDown(event: TimelinePointerEvent, _ctx: ToolContext): void;
|
|
2347
|
+
onPointerMove(event: TimelinePointerEvent, _ctx: ToolContext): ProvisionalState | null;
|
|
2348
|
+
onPointerUp(_event: TimelinePointerEvent, _ctx: ToolContext): Transaction | null;
|
|
2349
|
+
onKeyDown(_event: TimelineKeyEvent, _ctx: ToolContext): Transaction | null;
|
|
2350
|
+
onKeyUp(_event: TimelineKeyEvent, _ctx: ToolContext): void;
|
|
2351
|
+
/**
|
|
2352
|
+
* Resets per-drag state only.
|
|
2353
|
+
* scrollCallback is NOT cleared — it persists across cancels.
|
|
2354
|
+
* Re-registering on every cancelled drag would be unnecessary UI burden.
|
|
2355
|
+
*/
|
|
2356
|
+
onCancel(): void;
|
|
2357
|
+
}
|
|
2358
|
+
|
|
2359
|
+
/**
|
|
2360
|
+
* TransitionTool — Phase 4 Step 4
|
|
2361
|
+
*
|
|
2362
|
+
* Drag from a clip's right edge to create or resize a transition.
|
|
2363
|
+
* Click on an existing transition area to delete it.
|
|
2364
|
+
*
|
|
2365
|
+
* RULES:
|
|
2366
|
+
* - onPointerMove never dispatches; returns ProvisionalState for preview
|
|
2367
|
+
* - onPointerUp never mutates instance state (capture-before-reset)
|
|
2368
|
+
* - Every instance variable reset in onCancel()
|
|
2369
|
+
*/
|
|
2370
|
+
|
|
2371
|
+
declare class TransitionTool implements ITool {
|
|
2372
|
+
readonly id: ToolId;
|
|
2373
|
+
readonly shortcutKey: string;
|
|
2374
|
+
private pendingClipId;
|
|
2375
|
+
private dragStartX;
|
|
2376
|
+
private pendingDeleteTransitionClipId;
|
|
2377
|
+
getCursor(_ctx: ToolContext): string;
|
|
2378
|
+
getSnapCandidateTypes(): readonly SnapPointType[];
|
|
2379
|
+
onPointerDown(event: TimelinePointerEvent, ctx: ToolContext): void;
|
|
2380
|
+
onPointerMove(event: TimelinePointerEvent, ctx: ToolContext): ProvisionalState | null;
|
|
2381
|
+
onPointerUp(event: TimelinePointerEvent, ctx: ToolContext): Transaction | null;
|
|
2382
|
+
onKeyDown(_event: TimelineKeyEvent, _ctx: ToolContext): Transaction | null;
|
|
2383
|
+
onKeyUp(_event: TimelineKeyEvent, _ctx: ToolContext): void;
|
|
2384
|
+
onCancel(): void;
|
|
2385
|
+
}
|
|
2386
|
+
|
|
2387
|
+
/**
|
|
2388
|
+
* KeyframeTool (Pen tool) — Phase 4 Step 4
|
|
2389
|
+
*
|
|
2390
|
+
* Click on a clip's effect lane to add a keyframe.
|
|
2391
|
+
* Click an existing keyframe to delete (via Delete key). Drag keyframe to move.
|
|
2392
|
+
*
|
|
2393
|
+
* RULES:
|
|
2394
|
+
* - onPointerMove never dispatches; returns ProvisionalState for preview
|
|
2395
|
+
* - onPointerUp never mutates instance state (capture-before-reset)
|
|
2396
|
+
* - Every instance variable reset in onCancel()
|
|
2397
|
+
*/
|
|
2398
|
+
|
|
2399
|
+
declare class KeyframeTool implements ITool {
|
|
2400
|
+
readonly id: ToolId;
|
|
2401
|
+
readonly shortcutKey: string;
|
|
2402
|
+
private draggingKeyframe;
|
|
2403
|
+
private activeClipId;
|
|
2404
|
+
private activeEffectId;
|
|
2405
|
+
private pendingAddKeyframe;
|
|
2406
|
+
getCursor(_ctx: ToolContext): string;
|
|
2407
|
+
getSnapCandidateTypes(): readonly SnapPointType[];
|
|
2408
|
+
onPointerDown(event: TimelinePointerEvent, ctx: ToolContext): void;
|
|
2409
|
+
onPointerMove(event: TimelinePointerEvent, ctx: ToolContext): ProvisionalState | null;
|
|
2410
|
+
onPointerUp(event: TimelinePointerEvent, ctx: ToolContext): Transaction | null;
|
|
2411
|
+
onKeyDown(event: TimelineKeyEvent, ctx: ToolContext): Transaction | null;
|
|
2412
|
+
onKeyUp(_event: TimelineKeyEvent, _ctx: ToolContext): void;
|
|
2413
|
+
onCancel(): void;
|
|
2414
|
+
}
|
|
2415
|
+
|
|
2416
|
+
/**
|
|
2417
|
+
* SlideTool — Phase 7 Step 5
|
|
2418
|
+
*
|
|
2419
|
+
* Moves a clip left/right on the timeline. Neighbors trim to fill:
|
|
2420
|
+
* left neighbor's end resizes to abut; right neighbor moves and resizes.
|
|
2421
|
+
* No ripple — total duration unchanged.
|
|
2422
|
+
*
|
|
2423
|
+
* Uses: MOVE_CLIP, RESIZE_CLIP (edge 'start' | 'end').
|
|
2424
|
+
* Capture-before-reset in onPointerUp.
|
|
2425
|
+
*/
|
|
2426
|
+
|
|
2427
|
+
declare class SlideTool implements ITool {
|
|
2428
|
+
readonly id: ToolId;
|
|
2429
|
+
readonly shortcutKey = "Y";
|
|
2430
|
+
private draggingClipId;
|
|
2431
|
+
private dragStartX;
|
|
2432
|
+
private originalStart;
|
|
2433
|
+
getCursor(_ctx: ToolContext): string;
|
|
2434
|
+
getSnapCandidateTypes(): readonly SnapPointType[];
|
|
2435
|
+
onPointerDown(event: TimelinePointerEvent, _ctx: ToolContext): void;
|
|
2436
|
+
onPointerMove(event: TimelinePointerEvent, ctx: ToolContext): ProvisionalState | null;
|
|
2437
|
+
onPointerUp(event: TimelinePointerEvent, ctx: ToolContext): Transaction | null;
|
|
2438
|
+
onKeyDown(_event: TimelineKeyEvent, _ctx: ToolContext): Transaction | null;
|
|
2439
|
+
onKeyUp(_event: TimelineKeyEvent, _ctx: ToolContext): void;
|
|
2440
|
+
onCancel(): void;
|
|
2441
|
+
}
|
|
2442
|
+
|
|
2443
|
+
/**
|
|
2444
|
+
* ZoomTool — Phase 7 Step 5
|
|
2445
|
+
*
|
|
2446
|
+
* Adjusts pixelsPerFrame (zoom level) via callback only.
|
|
2447
|
+
* Does NOT dispatch any operations; pixelsPerFrame is UI state.
|
|
2448
|
+
*/
|
|
2449
|
+
|
|
2450
|
+
type ZoomToolOptions = {
|
|
2451
|
+
onZoomChange: (pixelsPerFrame: number) => void;
|
|
2452
|
+
minPixelsPerFrame?: number;
|
|
2453
|
+
maxPixelsPerFrame?: number;
|
|
2454
|
+
initialPixelsPerFrame?: number;
|
|
2455
|
+
};
|
|
2456
|
+
declare class ZoomTool implements ITool {
|
|
2457
|
+
readonly id: ToolId;
|
|
2458
|
+
readonly shortcutKey = "Z";
|
|
2459
|
+
private dragStartX;
|
|
2460
|
+
private dragStartZoom;
|
|
2461
|
+
private readonly options;
|
|
2462
|
+
constructor(options: ZoomToolOptions);
|
|
2463
|
+
getCursor(_ctx: ToolContext): string;
|
|
2464
|
+
getSnapCandidateTypes(): readonly SnapPointType[];
|
|
2465
|
+
onPointerDown(event: TimelinePointerEvent, ctx: ToolContext): void;
|
|
2466
|
+
onPointerMove(event: TimelinePointerEvent, ctx: ToolContext): null;
|
|
2467
|
+
onPointerUp(_event: TimelinePointerEvent, _ctx: ToolContext): null;
|
|
2468
|
+
onKeyDown(event: TimelineKeyEvent, ctx: ToolContext): null;
|
|
2469
|
+
onKeyUp(_event: TimelineKeyEvent, _ctx: ToolContext): void;
|
|
2470
|
+
onCancel(): void;
|
|
2471
|
+
}
|
|
2472
|
+
/** Returns an ITool that wraps ZoomTool with the given options (for host registration). */
|
|
2473
|
+
declare function createZoomTool(options: ZoomToolOptions): ITool;
|
|
2474
|
+
|
|
2475
|
+
/**
|
|
2476
|
+
* SerializationError — Phase 5 Step 1
|
|
2477
|
+
*
|
|
2478
|
+
* Thrown when deserialization or migration fails.
|
|
2479
|
+
*/
|
|
2480
|
+
|
|
2481
|
+
declare class SerializationError extends Error {
|
|
2482
|
+
readonly violations?: InvariantViolation[] | undefined;
|
|
2483
|
+
constructor(message: string, violations?: InvariantViolation[] | undefined);
|
|
2484
|
+
}
|
|
2485
|
+
|
|
2486
|
+
/**
|
|
2487
|
+
* Timeline serialization — Phase 5 Step 1
|
|
2488
|
+
*
|
|
2489
|
+
* Pure functions. No IO. No DOM. No external deps.
|
|
2490
|
+
* serializeTimeline / deserializeTimeline round-trip TimelineState.
|
|
2491
|
+
*/
|
|
2492
|
+
|
|
2493
|
+
/**
|
|
2494
|
+
* Serialize state to JSON string.
|
|
2495
|
+
* Converts assetRegistry Map to plain object for JSON compatibility.
|
|
2496
|
+
*/
|
|
2497
|
+
declare function serializeTimeline(state: TimelineState): string;
|
|
2498
|
+
/**
|
|
2499
|
+
* Parse JSON string, migrate to current schema, validate invariants.
|
|
2500
|
+
* Throws SerializationError on invalid JSON, missing schemaVersion,
|
|
2501
|
+
* unknown version, or invariant violations.
|
|
2502
|
+
*/
|
|
2503
|
+
declare function deserializeTimeline(raw: string): TimelineState;
|
|
2504
|
+
type AssetRemapCallback = (asset: FileAsset) => FileAsset;
|
|
2505
|
+
/**
|
|
2506
|
+
* Walk assetRegistry; for each FileAsset replace with remap(asset).
|
|
2507
|
+
* GeneratorAssets unchanged. Returns new state (immutable).
|
|
2508
|
+
*/
|
|
2509
|
+
declare function remapAssetPaths(state: TimelineState, remap: AssetRemapCallback): TimelineState;
|
|
2510
|
+
type OfflineAsset = {
|
|
2511
|
+
readonly assetId: AssetId;
|
|
2512
|
+
readonly path: string;
|
|
2513
|
+
readonly clipIds: readonly ClipId[];
|
|
2514
|
+
};
|
|
2515
|
+
/**
|
|
2516
|
+
* For each FileAsset where isOnline(asset) === false, collect clip IDs
|
|
2517
|
+
* that reference it. Host provides isOnline; core does not do filesystem checks.
|
|
2518
|
+
*/
|
|
2519
|
+
declare function findOfflineAssets(state: TimelineState, isOnline: (asset: FileAsset) => boolean): OfflineAsset[];
|
|
2520
|
+
|
|
2521
|
+
/**
|
|
2522
|
+
* OTIO export — Phase 5 Step 2
|
|
2523
|
+
*
|
|
2524
|
+
* Pure function. Produces OTIO JSON-serializable document from TimelineState.
|
|
2525
|
+
* No external OTIO library. Hand-rolled mapping.
|
|
2526
|
+
*/
|
|
2527
|
+
|
|
2528
|
+
type OTIORationalTime = {
|
|
2529
|
+
value: number;
|
|
2530
|
+
rate: number;
|
|
2531
|
+
};
|
|
2532
|
+
type OTIOTimeRange = {
|
|
2533
|
+
OTIO_SCHEMA: string;
|
|
2534
|
+
start_time: OTIORationalTime;
|
|
2535
|
+
duration: OTIORationalTime;
|
|
2536
|
+
};
|
|
2537
|
+
type OTIOExternalReference = {
|
|
2538
|
+
OTIO_SCHEMA: string;
|
|
2539
|
+
target_url: string;
|
|
2540
|
+
available_range: OTIOTimeRange;
|
|
2541
|
+
};
|
|
2542
|
+
type OTIOGeneratorReference = {
|
|
2543
|
+
OTIO_SCHEMA: string;
|
|
2544
|
+
generator_kind: string;
|
|
2545
|
+
};
|
|
2546
|
+
type OTIOMissingReference = {
|
|
2547
|
+
OTIO_SCHEMA: string;
|
|
2548
|
+
};
|
|
2549
|
+
type OTIOClip = {
|
|
2550
|
+
OTIO_SCHEMA: string;
|
|
2551
|
+
name: string;
|
|
2552
|
+
source_range: OTIOTimeRange;
|
|
2553
|
+
media_reference: OTIOExternalReference | OTIOGeneratorReference | OTIOMissingReference;
|
|
2554
|
+
effects?: OTIOEffect[];
|
|
2555
|
+
};
|
|
2556
|
+
type OTIOGap = {
|
|
2557
|
+
OTIO_SCHEMA: string;
|
|
2558
|
+
source_range: OTIOTimeRange;
|
|
2559
|
+
};
|
|
2560
|
+
type OTIOEffect = {
|
|
2561
|
+
OTIO_SCHEMA: string;
|
|
2562
|
+
name: string;
|
|
2563
|
+
effect_name: string;
|
|
2564
|
+
enabled: boolean;
|
|
2565
|
+
metadata: {
|
|
2566
|
+
params: readonly {
|
|
2567
|
+
key: string;
|
|
2568
|
+
value: number | string | boolean;
|
|
2569
|
+
}[];
|
|
2570
|
+
};
|
|
2571
|
+
};
|
|
2572
|
+
type OTIOTrack = {
|
|
2573
|
+
OTIO_SCHEMA: string;
|
|
2574
|
+
kind: string;
|
|
2575
|
+
children: (OTIOClip | OTIOGap)[];
|
|
2576
|
+
};
|
|
2577
|
+
type OTIOStack = {
|
|
2578
|
+
OTIO_SCHEMA: string;
|
|
2579
|
+
children: OTIOTrack[];
|
|
2580
|
+
};
|
|
2581
|
+
type OTIOMarker = {
|
|
2582
|
+
OTIO_SCHEMA: string;
|
|
2583
|
+
name: string;
|
|
2584
|
+
color: string;
|
|
2585
|
+
marked_range: OTIOTimeRange;
|
|
2586
|
+
};
|
|
2587
|
+
type OTIODocument = {
|
|
2588
|
+
OTIO_SCHEMA: string;
|
|
2589
|
+
name: string;
|
|
2590
|
+
global_start_time: OTIORationalTime;
|
|
2591
|
+
tracks: OTIOStack;
|
|
2592
|
+
markers: OTIOMarker[];
|
|
2593
|
+
};
|
|
2594
|
+
/**
|
|
2595
|
+
* Export TimelineState to an OTIO document (plain object).
|
|
2596
|
+
* Caller can JSON.stringify the result.
|
|
2597
|
+
*/
|
|
2598
|
+
declare function exportToOTIO(state: TimelineState): OTIODocument;
|
|
2599
|
+
|
|
2600
|
+
/**
|
|
2601
|
+
* OTIO import — Phase 5 Step 2
|
|
2602
|
+
*
|
|
2603
|
+
* Pure function. Builds TimelineState from OTIO document.
|
|
2604
|
+
* Throws SerializationError on invalid doc or invariant violations.
|
|
2605
|
+
*/
|
|
2606
|
+
|
|
2607
|
+
type OTIOImportOptions = {
|
|
2608
|
+
/** Override fps; default: from doc global_start_time.rate or first clip rate, fallback 30 */
|
|
2609
|
+
fps?: number;
|
|
2610
|
+
/** Override timeline name */
|
|
2611
|
+
name?: string;
|
|
2612
|
+
};
|
|
2613
|
+
declare function importFromOTIO(doc: unknown, options?: OTIOImportOptions): TimelineState;
|
|
2614
|
+
|
|
2615
|
+
/**
|
|
2616
|
+
* CMX3600 EDL export — Phase 5 Step 3
|
|
2617
|
+
*
|
|
2618
|
+
* Single video track only. Pure function, returns string.
|
|
2619
|
+
* No IO.
|
|
2620
|
+
*/
|
|
2621
|
+
|
|
2622
|
+
type EDLExportOptions = {
|
|
2623
|
+
/** Default: state.timeline.name */
|
|
2624
|
+
title?: string;
|
|
2625
|
+
/** Default: false (non-drop). True = 29.97 drop frame only. */
|
|
2626
|
+
dropFrame?: boolean;
|
|
2627
|
+
/** Which video track to export. Default: 0 (first video track). */
|
|
2628
|
+
trackIndex?: number;
|
|
2629
|
+
};
|
|
2630
|
+
/**
|
|
2631
|
+
* Convert frame count to timecode string.
|
|
2632
|
+
* dropFrame true: only 29.97fps uses real drop-frame; others fall back to non-drop.
|
|
2633
|
+
*/
|
|
2634
|
+
declare function frameToTimecode(frame: number, fps: number, dropFrame: boolean): string;
|
|
2635
|
+
/**
|
|
2636
|
+
* FileAsset: filename without extension, truncate 8 chars, uppercase.
|
|
2637
|
+
* GeneratorAsset or undefined: "AX".
|
|
2638
|
+
*/
|
|
2639
|
+
declare function reelName(asset: Asset | undefined): string;
|
|
2640
|
+
/**
|
|
2641
|
+
* Export a single video track to CMX3600 EDL string.
|
|
2642
|
+
* trackIndex selects which video track (default 0).
|
|
2643
|
+
*/
|
|
2644
|
+
declare function exportToEDL(state: TimelineState, options?: EDLExportOptions): string;
|
|
2645
|
+
|
|
2646
|
+
/**
|
|
2647
|
+
* AAF XML export — Phase 5 Step 4
|
|
2648
|
+
*
|
|
2649
|
+
* Simplified AAF XML representation for Avid interchange.
|
|
2650
|
+
* Pure function, returns string. No IO.
|
|
2651
|
+
*/
|
|
2652
|
+
|
|
2653
|
+
type AAFExportOptions = {
|
|
2654
|
+
/** Default: timeline name */
|
|
2655
|
+
projectName?: string;
|
|
2656
|
+
/** Default: derived from state (e.g. 30 → "30/1") */
|
|
2657
|
+
frameRate?: string;
|
|
2658
|
+
};
|
|
2659
|
+
declare function exportToAAF(state: TimelineState, options?: AAFExportOptions): string;
|
|
2660
|
+
|
|
2661
|
+
/**
|
|
2662
|
+
* FCP XML (FCPX) export — Phase 5 Step 4
|
|
2663
|
+
*
|
|
2664
|
+
* Final Cut Pro XML 1.10 interchange. Pure function, returns string. No IO.
|
|
2665
|
+
*/
|
|
2666
|
+
|
|
2667
|
+
type FCPXMLExportOptions = {
|
|
2668
|
+
libraryName?: string;
|
|
2669
|
+
eventName?: string;
|
|
2670
|
+
};
|
|
2671
|
+
/**
|
|
2672
|
+
* FCPXML rational time: "0s" or "{frames}/{fps}s".
|
|
2673
|
+
*/
|
|
2674
|
+
declare function toFCPTime(frames: number, fps: number): string;
|
|
2675
|
+
declare function exportToFCPXML(state: TimelineState, options?: FCPXMLExportOptions): string;
|
|
2676
|
+
|
|
2677
|
+
/**
|
|
2678
|
+
* PROJECT MODEL — Phase 5 Step 5
|
|
2679
|
+
*
|
|
2680
|
+
* A Project is a multi-timeline container with a shared bin hierarchy.
|
|
2681
|
+
* Pure types + factories only. No IO.
|
|
2682
|
+
*/
|
|
2683
|
+
|
|
2684
|
+
type ProjectId = string & {
|
|
2685
|
+
readonly __brand: 'ProjectId';
|
|
2686
|
+
};
|
|
2687
|
+
declare function toProjectId(s: string): ProjectId;
|
|
2688
|
+
type BinId = string & {
|
|
2689
|
+
readonly __brand: 'BinId';
|
|
2690
|
+
};
|
|
2691
|
+
declare function toBinId(s: string): BinId;
|
|
2692
|
+
type BinItem = {
|
|
2693
|
+
readonly kind: 'asset';
|
|
2694
|
+
readonly assetId: AssetId;
|
|
2695
|
+
} | {
|
|
2696
|
+
readonly kind: 'sequence';
|
|
2697
|
+
readonly timelineId: string;
|
|
2698
|
+
} | {
|
|
2699
|
+
readonly kind: 'bin';
|
|
2700
|
+
readonly binId: BinId;
|
|
2701
|
+
};
|
|
2702
|
+
type Bin = {
|
|
2703
|
+
readonly id: BinId;
|
|
2704
|
+
readonly label: string;
|
|
2705
|
+
readonly parentId: BinId | null;
|
|
2706
|
+
readonly items: readonly BinItem[];
|
|
2707
|
+
readonly color?: string;
|
|
2708
|
+
};
|
|
2709
|
+
declare function createBin(id: BinId, label: string, parentId?: BinId | null): Bin;
|
|
2710
|
+
type Project = {
|
|
2711
|
+
readonly id: ProjectId;
|
|
2712
|
+
readonly name: string;
|
|
2713
|
+
readonly timelines: readonly TimelineState[];
|
|
2714
|
+
readonly bins: readonly Bin[];
|
|
2715
|
+
readonly rootBinIds: readonly BinId[];
|
|
2716
|
+
readonly createdAt: number;
|
|
2717
|
+
readonly updatedAt: number;
|
|
2718
|
+
readonly schemaVersion: number;
|
|
2719
|
+
};
|
|
2720
|
+
declare function createProject(id: ProjectId, name: string, timelines?: readonly TimelineState[]): Project;
|
|
2721
|
+
|
|
2722
|
+
declare function addTimeline(project: Project, state: TimelineState): Project;
|
|
2723
|
+
declare function removeTimeline(project: Project, timelineId: string): Project;
|
|
2724
|
+
declare function addBin(project: Project, bin: Bin): Project;
|
|
2725
|
+
declare function removeBin(project: Project, binId: BinId): Project;
|
|
2726
|
+
declare function addItemToBin(project: Project, binId: BinId, item: BinItem): Project;
|
|
2727
|
+
declare function removeItemFromBin(project: Project, binId: BinId, item: BinItem): Project;
|
|
2728
|
+
declare function moveItemBetweenBins(project: Project, fromBinId: BinId, toBinId: BinId, item: BinItem): Project;
|
|
2729
|
+
|
|
2730
|
+
/**
|
|
2731
|
+
* Project serialization — Phase 5 Step 5
|
|
2732
|
+
*
|
|
2733
|
+
* Pure functions. No IO. Uses timeline migrate() + checkInvariants().
|
|
2734
|
+
*/
|
|
2735
|
+
|
|
2736
|
+
declare function serializeProject(project: Project): string;
|
|
2737
|
+
declare function deserializeProject(raw: string): Project;
|
|
2738
|
+
|
|
2739
|
+
/**
|
|
2740
|
+
* Playhead types — Phase 6 Step 1 + Step 5
|
|
2741
|
+
*
|
|
2742
|
+
* Playback position and quality. No DOM deps.
|
|
2743
|
+
*/
|
|
2744
|
+
|
|
2745
|
+
type PlaybackRate = number;
|
|
2746
|
+
type PlaybackQuality = 'full' | 'half' | 'quarter' | 'proxy';
|
|
2747
|
+
type LoopRegion = {
|
|
2748
|
+
readonly startFrame: TimelineFrame;
|
|
2749
|
+
readonly endFrame: TimelineFrame;
|
|
2750
|
+
};
|
|
2751
|
+
type PlayheadState = {
|
|
2752
|
+
readonly currentFrame: TimelineFrame;
|
|
2753
|
+
readonly isPlaying: boolean;
|
|
2754
|
+
readonly playbackRate: PlaybackRate;
|
|
2755
|
+
readonly quality: PlaybackQuality;
|
|
2756
|
+
readonly durationFrames: number;
|
|
2757
|
+
readonly fps: number;
|
|
2758
|
+
readonly loopRegion: LoopRegion | null;
|
|
2759
|
+
readonly prerollFrames: number;
|
|
2760
|
+
readonly postrollFrames: number;
|
|
2761
|
+
};
|
|
2762
|
+
type PlayheadEventType = 'play' | 'pause' | 'seek' | 'loop' | 'frame-dropped' | 'ended' | 'loop-point' | 'state';
|
|
2763
|
+
type PlayheadEvent = {
|
|
2764
|
+
readonly type: PlayheadEventType;
|
|
2765
|
+
readonly frame: TimelineFrame;
|
|
2766
|
+
readonly data?: unknown;
|
|
2767
|
+
};
|
|
2768
|
+
type PlayheadListener = (event: PlayheadEvent) => void;
|
|
2769
|
+
/** Return type of PlayheadController.on() — call to unsubscribe. */
|
|
2770
|
+
type PlayheadUnsubscribe = () => void;
|
|
2771
|
+
|
|
2772
|
+
/**
|
|
2773
|
+
* Clock abstraction — Phase 6 Step 1
|
|
2774
|
+
*
|
|
2775
|
+
* Allows PlayheadController to run without real rAF (swapped for mock in tests).
|
|
2776
|
+
*/
|
|
2777
|
+
type ClockCallback = (timestamp: number) => void;
|
|
2778
|
+
type Clock = {
|
|
2779
|
+
requestFrame: (cb: ClockCallback) => number;
|
|
2780
|
+
cancelFrame: (id: number) => void;
|
|
2781
|
+
now: () => number;
|
|
2782
|
+
};
|
|
2783
|
+
declare const browserClock: Clock;
|
|
2784
|
+
declare const nodeClock: Clock;
|
|
2785
|
+
declare function createTestClock(): {
|
|
2786
|
+
clock: Clock;
|
|
2787
|
+
tick: (ms: number) => void;
|
|
2788
|
+
getCallbacks: () => ClockCallback[];
|
|
2789
|
+
};
|
|
2790
|
+
|
|
2791
|
+
/**
|
|
2792
|
+
* PlayheadController — Phase 6 Step 1
|
|
2793
|
+
*
|
|
2794
|
+
* Manages playback position only. Decoupled from Dispatcher and TimelineState.
|
|
2795
|
+
* Never calls dispatch(). Uses clock abstraction for testability.
|
|
2796
|
+
*/
|
|
2797
|
+
|
|
2798
|
+
declare class PlayheadController {
|
|
2799
|
+
private state;
|
|
2800
|
+
private listeners;
|
|
2801
|
+
private rafId;
|
|
2802
|
+
private lastTimestamp;
|
|
2803
|
+
private frameAccum;
|
|
2804
|
+
private clock;
|
|
2805
|
+
constructor(initialState: Pick<PlayheadState, 'durationFrames' | 'fps'>, clock?: Clock);
|
|
2806
|
+
getState(): PlayheadState;
|
|
2807
|
+
play(): void;
|
|
2808
|
+
pause(): void;
|
|
2809
|
+
seekTo(frame: TimelineFrame): void;
|
|
2810
|
+
setPlaybackRate(rate: PlaybackRate): void;
|
|
2811
|
+
setQuality(quality: PlaybackQuality): void;
|
|
2812
|
+
setDuration(durationFrames: number): void;
|
|
2813
|
+
setLoopRegion(region: LoopRegion | null): void;
|
|
2814
|
+
setPreroll(frames: number): void;
|
|
2815
|
+
setPostroll(frames: number): void;
|
|
2816
|
+
private scheduleFrame;
|
|
2817
|
+
private onFrame;
|
|
2818
|
+
on(listener: PlayheadListener): PlayheadUnsubscribe;
|
|
2819
|
+
private emit;
|
|
2820
|
+
destroy(): void;
|
|
2821
|
+
}
|
|
2822
|
+
|
|
2823
|
+
/**
|
|
2824
|
+
* Pipeline contracts — Phase 6 Step 2
|
|
2825
|
+
*
|
|
2826
|
+
* Core defines the CONTRACT (types + interfaces).
|
|
2827
|
+
* Host app provides the IMPLEMENTATION.
|
|
2828
|
+
* Core never does actual decoding or compositing.
|
|
2829
|
+
*/
|
|
2830
|
+
|
|
2831
|
+
type VideoFrameRequest = {
|
|
2832
|
+
readonly clipId: ClipId;
|
|
2833
|
+
readonly mediaFrame: TimelineFrame;
|
|
2834
|
+
readonly quality: PlaybackQuality;
|
|
2835
|
+
};
|
|
2836
|
+
type AudioChunkRequest = {
|
|
2837
|
+
readonly clipId: ClipId;
|
|
2838
|
+
readonly mediaFrame: TimelineFrame;
|
|
2839
|
+
readonly durationFrames: number;
|
|
2840
|
+
readonly sampleRate: number;
|
|
2841
|
+
};
|
|
2842
|
+
type VideoFrameResult = {
|
|
2843
|
+
readonly clipId: ClipId;
|
|
2844
|
+
readonly mediaFrame: TimelineFrame;
|
|
2845
|
+
readonly width: number;
|
|
2846
|
+
readonly height: number;
|
|
2847
|
+
readonly bitmap: unknown;
|
|
2848
|
+
};
|
|
2849
|
+
type AudioChunkResult = {
|
|
2850
|
+
readonly clipId: ClipId;
|
|
2851
|
+
readonly mediaFrame: TimelineFrame;
|
|
2852
|
+
readonly samples: unknown;
|
|
2853
|
+
readonly sampleRate: number;
|
|
2854
|
+
};
|
|
2855
|
+
type VideoDecoder = (request: VideoFrameRequest) => Promise<VideoFrameResult>;
|
|
2856
|
+
type AudioDecoder = (request: AudioChunkRequest) => Promise<AudioChunkResult>;
|
|
2857
|
+
type CompositeLayer = {
|
|
2858
|
+
readonly clipId: ClipId;
|
|
2859
|
+
readonly trackId: TrackId;
|
|
2860
|
+
readonly trackIndex: number;
|
|
2861
|
+
readonly frame: VideoFrameResult;
|
|
2862
|
+
readonly transform: ClipTransform;
|
|
2863
|
+
readonly opacity: number;
|
|
2864
|
+
readonly blendMode: string;
|
|
2865
|
+
readonly effects: readonly Effect[];
|
|
2866
|
+
};
|
|
2867
|
+
/** Layer spec from resolveFrame (no decoded frame yet). */
|
|
2868
|
+
type ResolvedLayer = {
|
|
2869
|
+
readonly clipId: ClipId;
|
|
2870
|
+
readonly trackId: TrackId;
|
|
2871
|
+
readonly trackIndex: number;
|
|
2872
|
+
readonly mediaFrame: TimelineFrame;
|
|
2873
|
+
readonly transform: ClipTransform;
|
|
2874
|
+
readonly opacity: number;
|
|
2875
|
+
readonly blendMode: string;
|
|
2876
|
+
readonly effects: readonly Effect[];
|
|
2877
|
+
};
|
|
2878
|
+
type CompositeRequest = {
|
|
2879
|
+
readonly timelineFrame: TimelineFrame;
|
|
2880
|
+
readonly layers: readonly CompositeLayer[];
|
|
2881
|
+
readonly width: number;
|
|
2882
|
+
readonly height: number;
|
|
2883
|
+
readonly quality: PlaybackQuality;
|
|
2884
|
+
};
|
|
2885
|
+
type CompositeResult = {
|
|
2886
|
+
readonly timelineFrame: TimelineFrame;
|
|
2887
|
+
readonly bitmap: unknown;
|
|
2888
|
+
};
|
|
2889
|
+
/** Result of resolveFrame (layers have mediaFrame, not decoded frame). */
|
|
2890
|
+
type ResolvedCompositeRequest = {
|
|
2891
|
+
readonly timelineFrame: TimelineFrame;
|
|
2892
|
+
readonly layers: readonly ResolvedLayer[];
|
|
2893
|
+
readonly width: number;
|
|
2894
|
+
readonly height: number;
|
|
2895
|
+
readonly quality: PlaybackQuality;
|
|
2896
|
+
};
|
|
2897
|
+
type Compositor = (request: CompositeRequest) => Promise<CompositeResult>;
|
|
2898
|
+
type ThumbnailRequest = {
|
|
2899
|
+
readonly clipId: ClipId;
|
|
2900
|
+
readonly mediaFrame: TimelineFrame;
|
|
2901
|
+
readonly width: number;
|
|
2902
|
+
readonly height: number;
|
|
2903
|
+
};
|
|
2904
|
+
type ThumbnailResult = {
|
|
2905
|
+
readonly clipId: ClipId;
|
|
2906
|
+
readonly mediaFrame: TimelineFrame;
|
|
2907
|
+
readonly bitmap: unknown;
|
|
2908
|
+
};
|
|
2909
|
+
type ThumbnailProvider = (request: ThumbnailRequest) => Promise<ThumbnailResult>;
|
|
2910
|
+
type PipelineConfig = {
|
|
2911
|
+
readonly videoDecoder: VideoDecoder;
|
|
2912
|
+
readonly audioDecoder?: AudioDecoder;
|
|
2913
|
+
readonly compositor: Compositor;
|
|
2914
|
+
readonly thumbnailProvider?: ThumbnailProvider;
|
|
2915
|
+
};
|
|
2916
|
+
|
|
2917
|
+
/**
|
|
2918
|
+
* TrackIndex — Phase 7 Step 1
|
|
2919
|
+
*
|
|
2920
|
+
* Wraps IntervalTree per track for O(log n + k) getClipsAtFrame.
|
|
2921
|
+
*/
|
|
2922
|
+
|
|
2923
|
+
type ClipEntry = {
|
|
2924
|
+
clip: Clip;
|
|
2925
|
+
track: Track;
|
|
2926
|
+
trackIndex: number;
|
|
2927
|
+
};
|
|
2928
|
+
declare class TrackIndex {
|
|
2929
|
+
private tree;
|
|
2930
|
+
private built;
|
|
2931
|
+
build(state: TimelineState): void;
|
|
2932
|
+
query(frame: number): ClipEntry[];
|
|
2933
|
+
get isBuilt(): boolean;
|
|
2934
|
+
invalidate(): void;
|
|
2935
|
+
}
|
|
2936
|
+
|
|
2937
|
+
/**
|
|
2938
|
+
* Frame resolver — Phase 6 Step 2 + Step 3
|
|
2939
|
+
*
|
|
2940
|
+
* Given a TimelineFrame, resolves which clips are visible and builds
|
|
2941
|
+
* the composite request. Pure function — no async, no decoding.
|
|
2942
|
+
*/
|
|
2943
|
+
|
|
2944
|
+
/**
|
|
2945
|
+
* Returns the media-space frame for a clip at the given timeline frame.
|
|
2946
|
+
* mediaFrame = timelineFrame - clip.timelineStart + clip.mediaIn
|
|
2947
|
+
*/
|
|
2948
|
+
declare function mediaFrameForClip(clip: Clip, timelineFrame: TimelineFrame): TimelineFrame;
|
|
2949
|
+
/**
|
|
2950
|
+
* Returns all clips visible at the given timeline frame, with their
|
|
2951
|
+
* parent track and track index (z-order).
|
|
2952
|
+
* If index is provided and built, uses O(log n + k) lookup; else linear scan.
|
|
2953
|
+
*/
|
|
2954
|
+
declare function getClipsAtFrame(state: TimelineState, timelineFrame: TimelineFrame, index?: TrackIndex): Array<{
|
|
2955
|
+
clip: Clip;
|
|
2956
|
+
track: Track;
|
|
2957
|
+
trackIndex: number;
|
|
2958
|
+
}>;
|
|
2959
|
+
/**
|
|
2960
|
+
* Resolves the composite request for a timeline frame: which layers are
|
|
2961
|
+
* visible and their transform/opacity/blend/effects. Does not decode.
|
|
2962
|
+
* Pass optional index for O(log n + k) clip lookup.
|
|
2963
|
+
*/
|
|
2964
|
+
declare function resolveFrame(state: TimelineState, timelineFrame: TimelineFrame, quality: PlaybackQuality, dimensions: {
|
|
2965
|
+
width: number;
|
|
2966
|
+
height: number;
|
|
2967
|
+
}, index?: TrackIndex): ResolvedCompositeRequest;
|
|
2968
|
+
/**
|
|
2969
|
+
* Returns the nearest frame strictly after fromFrame where any clip
|
|
2970
|
+
* starts or ends on any track. Returns null if none.
|
|
2971
|
+
*/
|
|
2972
|
+
declare function findNextClipBoundary(state: TimelineState, fromFrame: TimelineFrame): TimelineFrame | null;
|
|
2973
|
+
/**
|
|
2974
|
+
* Returns the nearest frame strictly before fromFrame where any clip
|
|
2975
|
+
* starts or ends on any track. Returns null if none.
|
|
2976
|
+
*/
|
|
2977
|
+
declare function findPrevClipBoundary(state: TimelineState, fromFrame: TimelineFrame): TimelineFrame | null;
|
|
2978
|
+
/**
|
|
2979
|
+
* Returns the marker with the smallest anchor strictly after fromFrame.
|
|
2980
|
+
* Point markers use .frame; range markers use .frameStart as anchor.
|
|
2981
|
+
*/
|
|
2982
|
+
declare function findNextMarker(state: TimelineState, fromFrame: TimelineFrame): Marker | null;
|
|
2983
|
+
/**
|
|
2984
|
+
* Returns the marker with the largest anchor strictly before fromFrame.
|
|
2985
|
+
*/
|
|
2986
|
+
declare function findPrevMarker(state: TimelineState, fromFrame: TimelineFrame): Marker | null;
|
|
2987
|
+
/**
|
|
2988
|
+
* Searches all tracks for a clip with the given id.
|
|
2989
|
+
* Returns clip + parent track + trackIndex, or null if not found.
|
|
2990
|
+
*/
|
|
2991
|
+
declare function findClipById(state: TimelineState, clipId: ClipId): {
|
|
2992
|
+
clip: Clip;
|
|
2993
|
+
track: Track;
|
|
2994
|
+
trackIndex: number;
|
|
2995
|
+
} | null;
|
|
2996
|
+
|
|
2997
|
+
/**
|
|
2998
|
+
* Centered interval tree — Phase 7 Step 1
|
|
2999
|
+
*
|
|
3000
|
+
* Stores intervals [start, end) and answers query(point):
|
|
3001
|
+
* all intervals containing point. O(log n + k).
|
|
3002
|
+
*/
|
|
3003
|
+
type Interval<T> = {
|
|
3004
|
+
readonly start: number;
|
|
3005
|
+
readonly end: number;
|
|
3006
|
+
readonly data: T;
|
|
3007
|
+
};
|
|
3008
|
+
declare class IntervalTree<T> {
|
|
3009
|
+
private root;
|
|
3010
|
+
private _size;
|
|
3011
|
+
build(intervals: Interval<T>[]): void;
|
|
3012
|
+
query(point: number): T[];
|
|
3013
|
+
size(): number;
|
|
3014
|
+
}
|
|
3015
|
+
|
|
3016
|
+
declare class PlaybackEngine {
|
|
3017
|
+
private controller;
|
|
3018
|
+
private pipeline;
|
|
3019
|
+
private state;
|
|
3020
|
+
private dimensions;
|
|
3021
|
+
private trackIndex;
|
|
3022
|
+
private snapManager;
|
|
3023
|
+
private unsubscribe;
|
|
3024
|
+
constructor(state: TimelineState, pipeline: PipelineConfig, dimensions: {
|
|
3025
|
+
width: number;
|
|
3026
|
+
height: number;
|
|
3027
|
+
}, clock?: Clock);
|
|
3028
|
+
updateState(state: TimelineState): void;
|
|
3029
|
+
play(): void;
|
|
3030
|
+
pause(): void;
|
|
3031
|
+
seekTo(frame: TimelineFrame): void;
|
|
3032
|
+
seekToNextClipBoundary(): void;
|
|
3033
|
+
seekToPrevClipBoundary(): void;
|
|
3034
|
+
seekToNextMarker(): void;
|
|
3035
|
+
seekToPrevMarker(): void;
|
|
3036
|
+
seekToStart(): void;
|
|
3037
|
+
seekToEnd(): void;
|
|
3038
|
+
setPlaybackRate(rate: PlaybackRate): void;
|
|
3039
|
+
setQuality(quality: PlaybackQuality): void;
|
|
3040
|
+
setLoopRegion(region: LoopRegion | null): void;
|
|
3041
|
+
setPreroll(frames: number): void;
|
|
3042
|
+
setPostroll(frames: number): void;
|
|
3043
|
+
getState(): PlayheadState;
|
|
3044
|
+
getCurrentTimelineState(): TimelineState;
|
|
3045
|
+
on(listener: PlayheadListener): () => void;
|
|
3046
|
+
renderFrame(timelineFrame: TimelineFrame): Promise<CompositeResult>;
|
|
3047
|
+
destroy(): void;
|
|
3048
|
+
}
|
|
3049
|
+
|
|
3050
|
+
/**
|
|
3051
|
+
* Virtual rendering contract — Phase 7 Step 2
|
|
3052
|
+
*
|
|
3053
|
+
* Defines what is "visible" so the React layer can mount
|
|
3054
|
+
* only visible clip components.
|
|
3055
|
+
*/
|
|
3056
|
+
|
|
3057
|
+
type VirtualWindow = {
|
|
3058
|
+
readonly startFrame: TimelineFrame;
|
|
3059
|
+
readonly endFrame: TimelineFrame;
|
|
3060
|
+
readonly pixelsPerFrame: number;
|
|
3061
|
+
};
|
|
3062
|
+
type VirtualClipEntry = {
|
|
3063
|
+
readonly clip: Clip;
|
|
3064
|
+
readonly track: Track;
|
|
3065
|
+
readonly trackIndex: number;
|
|
3066
|
+
readonly isVisible: boolean;
|
|
3067
|
+
readonly left: number;
|
|
3068
|
+
readonly width: number;
|
|
3069
|
+
};
|
|
3070
|
+
/**
|
|
3071
|
+
* Returns all clips with visibility and layout (left, width).
|
|
3072
|
+
* Sorted by trackIndex ascending, then by clip timelineStart ascending.
|
|
3073
|
+
*/
|
|
3074
|
+
declare function getVisibleClips(state: TimelineState, window: VirtualWindow): VirtualClipEntry[];
|
|
3075
|
+
/**
|
|
3076
|
+
* Builds a VirtualWindow from viewport dimensions and scroll.
|
|
3077
|
+
*/
|
|
3078
|
+
declare function getVisibleFrameRange(viewportWidth: number, scrollLeft: number, pixelsPerFrame: number): VirtualWindow;
|
|
3079
|
+
|
|
3080
|
+
/**
|
|
3081
|
+
* StateChange diff — Phase 7 Step 2
|
|
3082
|
+
*
|
|
3083
|
+
* Lightweight diff for hook optimization: compare prev vs next
|
|
3084
|
+
* by reference so hooks can skip re-render when nothing relevant changed.
|
|
3085
|
+
*/
|
|
3086
|
+
|
|
3087
|
+
type StateChange = {
|
|
3088
|
+
readonly trackIds: boolean;
|
|
3089
|
+
readonly clipIds: ReadonlySet<ClipId>;
|
|
3090
|
+
readonly markers: boolean;
|
|
3091
|
+
readonly timeline: boolean;
|
|
3092
|
+
readonly playhead: boolean;
|
|
3093
|
+
};
|
|
3094
|
+
declare const EMPTY_STATE_CHANGE: StateChange;
|
|
3095
|
+
/**
|
|
3096
|
+
* Diffs prev and next state by reference.
|
|
3097
|
+
* clipIds: set of clip ids whose clip reference changed or were added/removed.
|
|
3098
|
+
*/
|
|
3099
|
+
declare function diffStates(prev: TimelineState, next: TimelineState): StateChange;
|
|
3100
|
+
|
|
3101
|
+
/**
|
|
3102
|
+
* Worker contracts — Phase 7 Step 4
|
|
3103
|
+
*
|
|
3104
|
+
* Core defines message/response types only.
|
|
3105
|
+
* No Worker instantiation — host responsibility.
|
|
3106
|
+
*/
|
|
3107
|
+
|
|
3108
|
+
type WaveformRequest = {
|
|
3109
|
+
readonly requestId: string;
|
|
3110
|
+
readonly assetId: AssetId;
|
|
3111
|
+
readonly channel: number;
|
|
3112
|
+
readonly startFrame: TimelineFrame;
|
|
3113
|
+
readonly endFrame: TimelineFrame;
|
|
3114
|
+
readonly buckets: number;
|
|
3115
|
+
readonly sampleRate: number;
|
|
3116
|
+
};
|
|
3117
|
+
type WaveformPeak = {
|
|
3118
|
+
readonly min: number;
|
|
3119
|
+
readonly max: number;
|
|
3120
|
+
readonly rms: number;
|
|
3121
|
+
};
|
|
3122
|
+
type WaveformResult = {
|
|
3123
|
+
readonly requestId: string;
|
|
3124
|
+
readonly assetId: AssetId;
|
|
3125
|
+
readonly peaks: readonly WaveformPeak[];
|
|
3126
|
+
readonly error?: string;
|
|
3127
|
+
};
|
|
3128
|
+
type WaveformWorkerMessage = {
|
|
3129
|
+
type: 'request';
|
|
3130
|
+
payload: WaveformRequest;
|
|
3131
|
+
} | {
|
|
3132
|
+
type: 'cancel';
|
|
3133
|
+
requestId: string;
|
|
3134
|
+
};
|
|
3135
|
+
type WaveformWorkerResponse = {
|
|
3136
|
+
type: 'result';
|
|
3137
|
+
payload: WaveformResult;
|
|
3138
|
+
} | {
|
|
3139
|
+
type: 'progress';
|
|
3140
|
+
requestId: string;
|
|
3141
|
+
progress: number;
|
|
3142
|
+
} | {
|
|
3143
|
+
type: 'error';
|
|
3144
|
+
requestId: string;
|
|
3145
|
+
message: string;
|
|
3146
|
+
};
|
|
3147
|
+
type ThumbnailPriority = 'high' | 'normal' | 'low';
|
|
3148
|
+
type ThumbnailQueueEntry = {
|
|
3149
|
+
readonly request: ThumbnailRequest;
|
|
3150
|
+
readonly priority: ThumbnailPriority;
|
|
3151
|
+
readonly addedAt: number;
|
|
3152
|
+
};
|
|
3153
|
+
type ThumbnailWorkerMessage = {
|
|
3154
|
+
type: 'request';
|
|
3155
|
+
payload: ThumbnailQueueEntry;
|
|
3156
|
+
} | {
|
|
3157
|
+
type: 'cancel';
|
|
3158
|
+
requestId: string;
|
|
3159
|
+
} | {
|
|
3160
|
+
type: 'set-priority';
|
|
3161
|
+
requestId: string;
|
|
3162
|
+
priority: ThumbnailPriority;
|
|
3163
|
+
};
|
|
3164
|
+
type ThumbnailWorkerResponse = {
|
|
3165
|
+
type: 'result';
|
|
3166
|
+
payload: ThumbnailResult;
|
|
3167
|
+
} | {
|
|
3168
|
+
type: 'error';
|
|
3169
|
+
requestId: string;
|
|
3170
|
+
message: string;
|
|
3171
|
+
};
|
|
3172
|
+
|
|
3173
|
+
/**
|
|
3174
|
+
* ThumbnailCache — Phase 7 Step 4
|
|
3175
|
+
*
|
|
3176
|
+
* In-memory LRU cache for thumbnail results.
|
|
3177
|
+
* No Worker — sits between pipeline and host's thumbnail provider.
|
|
3178
|
+
*/
|
|
3179
|
+
|
|
3180
|
+
declare class ThumbnailCache {
|
|
3181
|
+
private cache;
|
|
3182
|
+
private order;
|
|
3183
|
+
private maxSize;
|
|
3184
|
+
constructor(maxSize?: number);
|
|
3185
|
+
private key;
|
|
3186
|
+
get(request: ThumbnailRequest): ThumbnailResult | null;
|
|
3187
|
+
set(request: ThumbnailRequest, result: ThumbnailResult): void;
|
|
3188
|
+
has(request: ThumbnailRequest): boolean;
|
|
3189
|
+
invalidateClip(clipId: ClipId): void;
|
|
3190
|
+
clear(): void;
|
|
3191
|
+
get size(): number;
|
|
3192
|
+
}
|
|
3193
|
+
|
|
3194
|
+
/**
|
|
3195
|
+
* ThumbnailQueue — Phase 7 Step 4
|
|
3196
|
+
*
|
|
3197
|
+
* Priority queue for thumbnail requests.
|
|
3198
|
+
* Visible clips get 'high', off-screen get 'low'.
|
|
3199
|
+
*/
|
|
3200
|
+
|
|
3201
|
+
declare class ThumbnailQueue {
|
|
3202
|
+
private entries;
|
|
3203
|
+
enqueue(request: ThumbnailRequest, priority?: ThumbnailPriority): void;
|
|
3204
|
+
dequeue(): ThumbnailQueueEntry | null;
|
|
3205
|
+
cancel(clipId: ClipId): void;
|
|
3206
|
+
setPriority(clipId: ClipId, mediaFrame: TimelineFrame, priority: ThumbnailPriority): void;
|
|
3207
|
+
get length(): number;
|
|
3208
|
+
peek(): ThumbnailQueueEntry | null;
|
|
3209
|
+
clear(): void;
|
|
3210
|
+
}
|
|
3211
|
+
|
|
3212
|
+
/**
|
|
3213
|
+
* Keyboard contract — Phase 6 Step 4 + Step 5
|
|
3214
|
+
*
|
|
3215
|
+
* Key bindings and actions for J/K/L jog-shuttle and timeline navigation.
|
|
3216
|
+
*/
|
|
3217
|
+
|
|
3218
|
+
type TimelineKeyAction = 'play-pause' | 'stop' | 'jog-forward' | 'jog-backward' | 'jog-stop' | 'step-forward' | 'step-backward' | 'seek-start' | 'seek-end' | 'next-clip' | 'prev-clip' | 'next-marker' | 'prev-marker' | 'mark-in' | 'mark-out' | 'toggle-loop';
|
|
3219
|
+
type KeyBinding = {
|
|
3220
|
+
readonly code: string;
|
|
3221
|
+
readonly shift?: boolean;
|
|
3222
|
+
readonly alt?: boolean;
|
|
3223
|
+
readonly meta?: boolean;
|
|
3224
|
+
readonly ctrl?: boolean;
|
|
3225
|
+
readonly action: TimelineKeyAction;
|
|
3226
|
+
readonly repeat?: boolean;
|
|
3227
|
+
};
|
|
3228
|
+
declare const DEFAULT_KEY_BINDINGS: KeyBinding[];
|
|
3229
|
+
type KeyboardHandlerOptions = {
|
|
3230
|
+
bindings?: KeyBinding[];
|
|
3231
|
+
onMarkIn?: (frame: TimelineFrame) => void;
|
|
3232
|
+
onMarkOut?: (frame: TimelineFrame) => void;
|
|
3233
|
+
getTimelineState?: () => TimelineState;
|
|
3234
|
+
};
|
|
3235
|
+
|
|
3236
|
+
/**
|
|
3237
|
+
* KeyboardHandler — Phase 6 Step 4
|
|
3238
|
+
*
|
|
3239
|
+
* J/K/L jog-shuttle and keyboard contract. Zero DOM deps;
|
|
3240
|
+
* accepts TimelineKeyEvent (host maps from KeyboardEvent).
|
|
3241
|
+
*/
|
|
3242
|
+
|
|
3243
|
+
declare class KeyboardHandler {
|
|
3244
|
+
private bindings;
|
|
3245
|
+
private engine;
|
|
3246
|
+
private jogLevel;
|
|
3247
|
+
private onMarkIn;
|
|
3248
|
+
private onMarkOut;
|
|
3249
|
+
private getTimelineState;
|
|
3250
|
+
constructor(engine: PlaybackEngine, options?: KeyboardHandlerOptions);
|
|
3251
|
+
handleKeyDown(event: TimelineKeyEvent): boolean;
|
|
3252
|
+
private dispatchAction;
|
|
3253
|
+
}
|
|
3254
|
+
|
|
3255
|
+
export { type InvariantViolation as $, type Asset as A, type Bin as B, type Clip as C, DEFAULT_AUDIO_PROPERTIES as D, DEFAULT_CLIP_TRANSFORM as E, DEFAULT_COMPRESSION_POLICY as F, DEFAULT_KEY_BINDINGS as G, type DispatchResult as H, type EDLExportOptions as I, EMPTY_STATE_CHANGE as J, type EasingCurve as K, type Effect as L, type EffectId as M, type EffectParam as N, type EffectType as O, type FCPXMLExportOptions as P, type FrameRate as Q, FrameRates as R, HOLD_EASING as S, type TimelineState as T, HandTool as U, type HistoryEntry as V, HistoryStack as W, type HistoryState as X, type ITool as Y, type Interval as Z, IntervalTree as _, type Track as a, TimelineEngine as a$, type KeyBinding as a0, KeyboardHandler as a1, type KeyboardHandlerOptions as a2, type Keyframe as a3, type KeyframeId as a4, KeyframeTool as a5, LINEAR_EASING as a6, type LinkGroup as a7, type LinkGroupId as a8, type LoopRegion as a9, RippleInsertTool as aA, RippleTrimTool as aB, RollTrimTool as aC, type RubberBandRegion as aD, type SRTParseOptions as aE, SelectionTool as aF, type SequenceSettings as aG, SerializationError as aH, SlideTool as aI, SlipTool as aJ, type SnapIndex as aK, SnapIndexManager as aL, type SnapPoint as aM, type SnapPointType as aN, type StateChange as aO, ThumbnailCache as aP, type ThumbnailPriority as aQ, type ThumbnailProvider as aR, ThumbnailQueue as aS, type ThumbnailQueueEntry as aT, type ThumbnailRequest as aU, type ThumbnailResult as aV, type ThumbnailWorkerMessage as aW, type ThumbnailWorkerResponse as aX, type TimeRange as aY, type Timecode as aZ, type Timeline as a_, type Modifiers as aa, NO_COMPRESSION as ab, NoOpTool as ac, type OTIODocument as ad, type OTIOImportOptions as ae, type OfflineAsset as af, type OperationPrimitive as ag, type PipelineConfig as ah, PlaybackEngine as ai, type PlaybackQuality as aj, type PlaybackRate as ak, PlayheadController as al, type PlayheadEvent as am, type PlayheadEventType as an, type PlayheadListener as ao, type PlayheadState as ap, type PlayheadUnsubscribe as aq, type Project as ar, type ProjectId as as, type ProvisionalManager as at, type ProvisionalState as au, type RationalTime as av, RazorTool as aw, type RejectionReason as ax, type RenderStage as ay, RippleDeleteTool as az, type TimelineFrame as b, createZoomTool as b$, type TimelineKeyAction as b0, type TimelineKeyEvent as b1, type TimelinePointerEvent as b2, type ToolContext as b3, type ToolId as b4, type ToolRegistry as b5, type TrackGroup as b6, type TrackGroupId as b7, type TrackId as b8, TrackIndex as b9, addItemToBin as bA, addTimeline as bB, browserClock as bC, buildSnapIndex as bD, canRedo as bE, canUndo as bF, checkInvariants as bG, clampFrame as bH, clearProvisional as bI, clipContainsFrame as bJ, clipsOverlap as bK, createAnimatableProperty as bL, createAsset as bM, createBin as bN, createClip as bO, createEffect as bP, createHistory as bQ, createLinkGroup as bR, createProject as bS, createProvisionalManager as bT, createRegistry as bU, createTestClock as bV, createTimeline as bW, createTimelineState as bX, createTrack as bY, createTrackGroup as bZ, createTransition as b_, type TrackType as ba, type Transaction as bb, TransactionCompressor as bc, type Transition as bd, type TransitionAlignment as be, type TransitionId as bf, type TransitionParam as bg, TransitionTool as bh, type TransitionType as bi, type VTTParseOptions as bj, type VideoDecoder as bk, type VideoFrameRequest as bl, type VideoFrameResult as bm, type ViolationType as bn, type VirtualClipEntry as bo, type VirtualWindow as bp, type WaveformPeak as bq, type WaveformRequest as br, type WaveformResult as bs, type WaveformWorkerMessage as bt, type WaveformWorkerResponse as bu, ZoomTool as bv, type ZoomToolOptions as bw, activateTool as bx, addBin as by, addFrames as bz, type AAFExportOptions as c, toProjectId as c$, defaultCaptionStyle as c0, deserializeProject as c1, deserializeTimeline as c2, diffStates as c3, dispatch as c4, exportToAAF as c5, exportToEDL as c6, exportToFCPXML as c7, exportToOTIO as c8, findMarkersByColor as c9, parseSRT as cA, parseVTT as cB, pushHistory as cC, redo as cD, reelName as cE, registerTool as cF, remapAssetPaths as cG, removeBin as cH, removeItemFromBin as cI, removeTimeline as cJ, resolveClip as cK, resolveFrame as cL, secondsToFrames as cM, serializeProject as cN, serializeTimeline as cO, setProvisional as cP, sortTrackClips as cQ, subtitleImportToOps as cR, subtractFrames as cS, toAssetId as cT, toBinId as cU, toClipId as cV, toEffectId as cW, toFCPTime as cX, toFrame as cY, toKeyframeId as cZ, toLinkGroupId as c_, findMarkersByLabel as ca, findNextClipBoundary as cb, findNextMarker as cc, findOfflineAssets as cd, findPrevClipBoundary as ce, findPrevMarker as cf, frame as cg, frameDuration as ch, frameRate as ci, frameToTimecode as cj, framesToMinutesSeconds as ck, framesToSeconds as cl, framesToTimecode as cm, getActiveTool as cn, getClipDuration as co, getClipMediaDuration as cp, getCurrentState as cq, getVisibleClips as cr, getVisibleFrameRange as cs, importFromOTIO as ct, isDropFrame as cu, isValidFrame as cv, mediaFrameForClip as cw, moveItemBetweenBins as cx, nearest as cy, nodeClock as cz, type AnimatableProperty as d, toTimecode as d0, toToolId as d1, toTrackGroupId as d2, toTrackId as d3, toTransitionId as d4, toggleSnap as d5, undo as d6, findClipById as d7, getClipsAtFrame as d8, type AssetId as e, type AssetRegistry as f, type AssetRemapCallback as g, type AssetStatus as h, type AudioChunkRequest as i, type AudioChunkResult as j, type AudioDecoder as k, type AudioProperties as l, type BinId as m, type BinItem as n, CURRENT_SCHEMA_VERSION as o, type ChannelRouting as p, type ClipEntry as q, type ClipId as r, type ClipTransform as s, type Clock as t, type CompositeLayer as u, type CompositeRequest as v, type CompositeResult as w, type Compositor as x, type CompressibleOpType as y, type CompressionPolicy as z };
|