@cut-workbench/core 0.4.0 → 0.4.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.
Files changed (58) hide show
  1. package/dist/chunks/{canvas-size-Ct61OYjW.cjs → canvas-size-DIABzEW2.cjs} +212 -44
  2. package/dist/chunks/canvas-size-DIABzEW2.cjs.map +1 -0
  3. package/dist/chunks/{canvas-size-CM2oKApi.js → canvas-size-Dlnng74w.js} +168 -30
  4. package/dist/chunks/canvas-size-Dlnng74w.js.map +1 -0
  5. package/dist/chunks/id-CJsdlGei.js +35 -0
  6. package/dist/chunks/id-CJsdlGei.js.map +1 -0
  7. package/dist/chunks/id-DaCxD4-5.cjs +70 -0
  8. package/dist/chunks/id-DaCxD4-5.cjs.map +1 -0
  9. package/dist/chunks/{snap-DyIuO2hU.cjs → snap-Cfk15JVg.cjs} +2 -22
  10. package/dist/chunks/{snap-DyIuO2hU.cjs.map → snap-Cfk15JVg.cjs.map} +1 -1
  11. package/dist/chunks/{snap-CBV9G4xq.js → snap-DpkVPVJN.js} +3 -17
  12. package/dist/chunks/{snap-CBV9G4xq.js.map → snap-DpkVPVJN.js.map} +1 -1
  13. package/dist/chunks/thumbnails-DEjnao5r.cjs +81 -0
  14. package/dist/chunks/thumbnails-DEjnao5r.cjs.map +1 -0
  15. package/dist/chunks/thumbnails-qH0dX_Zs.js +64 -0
  16. package/dist/chunks/thumbnails-qH0dX_Zs.js.map +1 -0
  17. package/dist/chunks/{time-CnMfSVYX.cjs → time-CYEd91qN.cjs} +1 -69
  18. package/dist/chunks/time-CYEd91qN.cjs.map +1 -0
  19. package/dist/chunks/{time-C_gvkVl1.js → time-ChF1GwCy.js} +2 -34
  20. package/dist/chunks/time-ChF1GwCy.js.map +1 -0
  21. package/dist/chunks/{waveform-Ie-G5Lle.cjs → waveform-BYwDdKpC.cjs} +34 -15
  22. package/dist/chunks/waveform-BYwDdKpC.cjs.map +1 -0
  23. package/dist/chunks/{waveform-DJkRtMct.js → waveform-uyGNE01H.js} +32 -13
  24. package/dist/chunks/waveform-uyGNE01H.js.map +1 -0
  25. package/dist/engine/index.cjs +668 -76
  26. package/dist/engine/index.cjs.map +1 -1
  27. package/dist/engine/index.js +648 -57
  28. package/dist/engine/index.js.map +1 -1
  29. package/dist/engine/webav/exporter.d.ts +3 -1
  30. package/dist/engine/webav/import-files.d.ts +17 -2
  31. package/dist/engine/webav/import-pipeline.d.ts +10 -0
  32. package/dist/engine/webav/types.d.ts +15 -0
  33. package/dist/index.cjs +18 -13
  34. package/dist/index.cjs.map +1 -1
  35. package/dist/index.d.ts +1 -0
  36. package/dist/index.js +6 -5
  37. package/dist/index.js.map +1 -1
  38. package/dist/lib/error.d.ts +6 -0
  39. package/dist/media/thumbnails.d.ts +8 -0
  40. package/dist/project/schema.d.ts +16 -4
  41. package/dist/timeline/index.cjs +10 -6
  42. package/dist/timeline/index.d.ts +1 -0
  43. package/dist/timeline/index.js +4 -4
  44. package/dist/timeline/normalize.d.ts +28 -0
  45. package/dist/timeline/ops.d.ts +0 -4
  46. package/dist/timeline/snap.d.ts +0 -2
  47. package/dist/types.d.ts +14 -2
  48. package/package.json +1 -1
  49. package/dist/chunks/canvas-size-CM2oKApi.js.map +0 -1
  50. package/dist/chunks/canvas-size-Ct61OYjW.cjs.map +0 -1
  51. package/dist/chunks/import-pipeline-A8KkqtZT.cjs +0 -610
  52. package/dist/chunks/import-pipeline-A8KkqtZT.cjs.map +0 -1
  53. package/dist/chunks/import-pipeline-DLIdgrIb.js +0 -521
  54. package/dist/chunks/import-pipeline-DLIdgrIb.js.map +0 -1
  55. package/dist/chunks/time-C_gvkVl1.js.map +0 -1
  56. package/dist/chunks/time-CnMfSVYX.cjs.map +0 -1
  57. package/dist/chunks/waveform-DJkRtMct.js.map +0 -1
  58. package/dist/chunks/waveform-Ie-G5Lle.cjs.map +0 -1
@@ -1,11 +1,303 @@
1
1
  import { t as __exportAll } from "../chunks/rolldown-runtime-D8jQpDG7.js";
2
- import { i as asUs } from "../chunks/time-C_gvkVl1.js";
3
- import { A as createEmptyProject, N as getProjectDuration, O as clampStillDuration, m as pointInTransform } from "../chunks/canvas-size-CM2oKApi.js";
4
- import { A as createMemoryStorage, D as createWebBridge, E as resolveExportSettings, M as deserializeProject, N as serializeProject, S as ExportCancelledError, T as createExportFilename, _ as detectDependencies, a as selectAssetById, b as isEnvironmentUsable, c as selectClips, d as selectProjectDuration, f as selectSelectedClipIds, g as ENGINE_DEPENDENCIES_INSTALL_COMMAND, h as createEventBus, i as createEditorStore, l as selectPlayheadUs, m as createHistoryStore, o as selectAssets, p as selectTracks, s as selectClipById, u as selectProject, v as detectEnvironment, x as summarizeCapabilities, y as detectOpfs } from "../chunks/waveform-DJkRtMct.js";
5
- import { r as detectFileType, t as UnsupportedFileError } from "../chunks/file-reader-OjeI7HTg.js";
6
- import { a as loadAudioClipForAsset, c as createClipFromBuffer, d as createTextClip, f as TextClip, h as DEFAULT_TEXT_STYLE, l as createClipFromFile, m as DEFAULT_SUBTITLE_STYLE, n as createTextAsset, o as loadPcmForAsset, p as cssFromStyle, r as importFile, s as releaseThumbnails, t as createSubtitleAsset, u as createSubtitleClip } from "../chunks/import-pipeline-DLIdgrIb.js";
7
- import { Combinator, EmbedSubtitlesClip, ImgClip, MP4Clip, OffscreenSprite, VisibleSprite, fixFMP4Duration } from "@webav/av-cliper";
2
+ import { r as DEFAULT_STILL_DURATION_US, t as createId } from "../chunks/id-CJsdlGei.js";
3
+ import { i as asUs } from "../chunks/time-ChF1GwCy.js";
4
+ import { F as createEmptyProject, N as clampStillDuration, R as getProjectDuration, m as pointInTransform } from "../chunks/canvas-size-Dlnng74w.js";
5
+ import { A as createMemoryStorage, D as createWebBridge, E as resolveExportSettings, M as deserializeProject, N as serializeProject, S as ExportCancelledError, T as createExportFilename, _ as detectDependencies, a as selectAssetById, b as isEnvironmentUsable, c as selectClips, d as selectProjectDuration, f as selectSelectedClipIds, g as ENGINE_DEPENDENCIES_INSTALL_COMMAND, h as createEventBus, i as createEditorStore, l as selectPlayheadUs, m as createHistoryStore, n as computeWaveform, o as selectAssets, p as selectTracks, s as selectClipById, u as selectProject, v as detectEnvironment, x as summarizeCapabilities, y as detectOpfs } from "../chunks/waveform-uyGNE01H.js";
6
+ import { i as fileToStream, n as bufferToStream, r as detectFileType, t as UnsupportedFileError } from "../chunks/file-reader-OjeI7HTg.js";
7
+ import { n as releaseThumbnails, t as generateThumbnails } from "../chunks/thumbnails-qH0dX_Zs.js";
8
+ import { AudioClip, Combinator, EmbedSubtitlesClip, ImgClip, MP4Clip, OffscreenSprite, VisibleSprite, fixFMP4Duration, renderTxt2ImgBitmap } from "@webav/av-cliper";
8
9
  import { AVCanvas } from "@webav/av-canvas";
10
+ // src/engine/webav/styles.ts
11
+ var DEFAULT_TEXT_STYLE = {
12
+ fontSize: 64,
13
+ color: "#ffffff",
14
+ fontFamily: "sans-serif",
15
+ fontWeight: 600,
16
+ fontStyle: "normal",
17
+ strokeColor: "#000000",
18
+ strokeWidth: 0,
19
+ shadowColor: "rgba(0,0,0,0.6)",
20
+ shadowBlur: 8,
21
+ align: "center",
22
+ background: "transparent"
23
+ };
24
+ /**
25
+ * 字幕默认字体。
26
+ *
27
+ * ⚠️ 不要写 'Noto Sans SC' —— 大多数系统并未安装该字体,`EmbedSubtitlesClip` 会把
28
+ * `ctx.font` 设为 `30px Noto Sans SC`,此时浏览器回退到默认字体,中文可能显示为方框。
29
+ * 使用系统字体栈(sans-serif)由浏览器挑选可用的中文字体,最稳。
30
+ */
31
+ var DEFAULT_SUBTITLE_STYLE = {
32
+ color: "#ffffff",
33
+ textBgColor: null,
34
+ fontFamily: "sans-serif",
35
+ fontSize: 30,
36
+ strokeStyle: "#000000",
37
+ lineWidth: null,
38
+ bottomOffset: 30
39
+ };
40
+ // src/engine/webav/text-clip.ts
41
+ function cssFromStyle(style) {
42
+ const shadow = style.shadowBlur > 0 ? `text-shadow: 0 2px ${style.shadowBlur}px ${style.shadowColor};` : "";
43
+ const stroke = style.strokeWidth > 0 ? `-webkit-text-stroke: ${style.strokeWidth}px ${style.strokeColor};` : "";
44
+ return [
45
+ `font-size:${style.fontSize}px`,
46
+ `color:${style.color}`,
47
+ `font-family:${style.fontFamily}`,
48
+ `font-weight:${style.fontWeight}`,
49
+ `font-style:${style.fontStyle}`,
50
+ `text-align:${style.align}`,
51
+ `background:${style.background}`,
52
+ "line-height:1.25",
53
+ "padding:8px 16px",
54
+ "white-space:pre-wrap",
55
+ "max-width:80%",
56
+ stroke,
57
+ shadow
58
+ ].filter(Boolean).join(";");
59
+ }
60
+ /**
61
+ * 文字素材。内部缓存最近一次渲染的 ImageBitmap,按「文本 + 样式」做键避免重复渲染。
62
+ */
63
+ var TextClip = class TextClip {
64
+ ready;
65
+ opts;
66
+ metaValue;
67
+ cacheKey = "";
68
+ cachedBitmap = null;
69
+ destroyed = false;
70
+ constructor(options) {
71
+ this.opts = {
72
+ ...options,
73
+ style: {
74
+ ...DEFAULT_TEXT_STYLE,
75
+ ...options.style
76
+ },
77
+ animation: options.animation ?? "none",
78
+ charsPerSecond: options.charsPerSecond ?? 12,
79
+ animOffsetUs: options.animOffsetUs ?? 0
80
+ };
81
+ this.metaValue = {
82
+ width: options.width,
83
+ height: options.height,
84
+ duration: Math.max(1, options.durationUs)
85
+ };
86
+ this.ready = Promise.resolve(this.metaValue);
87
+ }
88
+ get meta() {
89
+ return this.metaValue;
90
+ }
91
+ visibleTextAt(timeUs) {
92
+ const { text, animation, charsPerSecond } = this.opts;
93
+ if (animation === "none") return text;
94
+ const phaseUs = timeUs + this.opts.animOffsetUs;
95
+ if (animation === "typewriter") {
96
+ const visible = Math.floor(phaseUs / 1e6 * charsPerSecond);
97
+ if (visible <= 0) return null;
98
+ return text.slice(0, visible);
99
+ }
100
+ return Math.floor(phaseUs / 1e6 * 2) % 2 === 0 ? text : null;
101
+ }
102
+ async tick(time) {
103
+ if (this.destroyed) return { state: "done" };
104
+ if (time >= this.opts.durationUs) return { state: "done" };
105
+ const visible = this.visibleTextAt(time);
106
+ if (visible == null || visible.length === 0) return {
107
+ video: await this.renderEmpty(),
108
+ state: "success"
109
+ };
110
+ const key = `${visible}|${JSON.stringify(this.opts.style)}`;
111
+ if (key !== this.cacheKey || this.cachedBitmap == null) {
112
+ this.cachedBitmap?.close();
113
+ this.cachedBitmap = await this.renderTextToFrame(visible);
114
+ this.cacheKey = key;
115
+ }
116
+ return {
117
+ video: await createImageBitmap(this.cachedBitmap),
118
+ state: "success"
119
+ };
120
+ }
121
+ /**
122
+ * 把文字渲染成「整帧」位图(工程分辨率)。
123
+ *
124
+ * 为什么不能直接把 renderTxt2ImgBitmap 的结果当帧用:
125
+ * 它返回的是文字自身尺寸的小图(几十到几百像素宽),Sprite 会把它拉伸到 rect 的
126
+ * 1280×720 —— 文字被放大后严重模糊,且居中位置由 Sprite 决定而非文字块决定。
127
+ * 因此这里先渲染文字小图,再按比例居中合成到工程尺寸的画布上。
128
+ */
129
+ async renderTextToFrame(text) {
130
+ const { width, height } = this.opts;
131
+ const canvas = new OffscreenCanvas(width, height);
132
+ const ctx = canvas.getContext("2d");
133
+ if (ctx == null) throw new Error("无法创建文字渲染上下文");
134
+ const layer = await renderTxt2ImgBitmap(text, cssFromStyle(this.opts.style));
135
+ const align = this.opts.style.align;
136
+ const margin = Math.round(width * .05);
137
+ const maxWidth = width - margin * 2;
138
+ const scale = layer.width > maxWidth ? maxWidth / layer.width : 1;
139
+ const drawW = Math.round(layer.width * scale);
140
+ const drawH = Math.round(layer.height * scale);
141
+ const x = align === "left" ? margin : align === "right" ? width - margin - drawW : (width - drawW) / 2;
142
+ const y = Math.round((height - drawH) / 2);
143
+ ctx.drawImage(layer, x, y, drawW, drawH);
144
+ layer.close();
145
+ return canvas.transferToImageBitmap();
146
+ }
147
+ emptyBitmap = null;
148
+ async renderEmpty() {
149
+ if (this.emptyBitmap != null) return this.emptyBitmap;
150
+ const canvas = new OffscreenCanvas(this.opts.width, this.opts.height);
151
+ this.emptyBitmap = canvas.transferToImageBitmap();
152
+ return this.emptyBitmap;
153
+ }
154
+ async split(time) {
155
+ if (time <= 0 || time >= this.opts.durationUs) throw new Error("\"time\" out of bounds");
156
+ const left = new TextClip({
157
+ ...this.opts,
158
+ durationUs: time
159
+ });
160
+ const right = new TextClip({
161
+ ...this.opts,
162
+ durationUs: this.opts.durationUs - time,
163
+ animOffsetUs: this.opts.animOffsetUs + time
164
+ });
165
+ await Promise.all([left.ready, right.ready]);
166
+ return [left, right];
167
+ }
168
+ async clone() {
169
+ const copy = new TextClip({ ...this.opts });
170
+ await copy.ready;
171
+ return copy;
172
+ }
173
+ destroy() {
174
+ this.destroyed = true;
175
+ this.cachedBitmap?.close();
176
+ this.cachedBitmap = null;
177
+ this.emptyBitmap?.close();
178
+ this.emptyBitmap = null;
179
+ }
180
+ };
181
+ // src/engine/webav/clip-factory.ts
182
+ /**
183
+ * File → IClip 的分派工厂。
184
+ *
185
+ * ⚠️ 本文件是 `@webav/*` 的合法引用点之一(另一个是 core/webav 下的其他模块)。
186
+ * 不要在 features / store / components 中直接 import WebAV。
187
+ *
188
+ * 所有 clip 实例的生命周期由 `ClipRegistry` 统一管理,这里只负责创建。
189
+ */
190
+ /**
191
+ * 创建素材的「母版」clip 并读取 meta。
192
+ *
193
+ * 对静态图/文字/字幕,`meta.duration` 可能是 Infinity,这里会归一成有限值:
194
+ * 真正的时长由时间轴 clip 决定,母版只用于 clone 与抽帧。
195
+ */
196
+ async function createClipFromFile(file, kind) {
197
+ switch (kind) {
198
+ case "video": {
199
+ const clip = new MP4Clip(fileToStream(file));
200
+ const meta = await clip.ready;
201
+ return {
202
+ clip,
203
+ meta: {
204
+ durationUs: meta.duration,
205
+ width: meta.width,
206
+ height: meta.height,
207
+ hasAudio: meta.audioChanCount > 0
208
+ }
209
+ };
210
+ }
211
+ case "audio": {
212
+ const clip = new AudioClip(fileToStream(file));
213
+ return {
214
+ clip,
215
+ meta: {
216
+ durationUs: (await clip.ready).duration,
217
+ width: 0,
218
+ height: 0,
219
+ hasAudio: true
220
+ }
221
+ };
222
+ }
223
+ case "image": {
224
+ const clip = new ImgClip(fileToStream(file));
225
+ const meta = await clip.ready;
226
+ return {
227
+ clip,
228
+ meta: {
229
+ durationUs: Number.isFinite(meta.duration) ? meta.duration : DEFAULT_STILL_DURATION_US,
230
+ width: meta.width,
231
+ height: meta.height,
232
+ hasAudio: false
233
+ }
234
+ };
235
+ }
236
+ case "subtitle": {
237
+ const text = await file.text();
238
+ const clip = new EmbedSubtitlesClip(text, {
239
+ videoWidth: 1280,
240
+ videoHeight: 720
241
+ });
242
+ const meta = await clip.ready;
243
+ return {
244
+ clip,
245
+ meta: {
246
+ durationUs: Number.isFinite(meta.duration) ? meta.duration : DEFAULT_STILL_DURATION_US,
247
+ width: meta.width,
248
+ height: meta.height,
249
+ hasAudio: false
250
+ }
251
+ };
252
+ }
253
+ default: throw new Error(`Unsupported asset kind: ${kind}`);
254
+ }
255
+ }
256
+ /** 文字素材:不走文件,直接由 TextClip 生成 */
257
+ function createTextClip(init) {
258
+ return new TextClip({
259
+ text: init.text,
260
+ style: init.style,
261
+ width: init.width,
262
+ height: init.height,
263
+ durationUs: init.durationUs
264
+ });
265
+ }
266
+ function createSubtitleClip(init) {
267
+ return new EmbedSubtitlesClip(init.srt, {
268
+ videoWidth: init.width,
269
+ videoHeight: init.height,
270
+ color: init.style.color,
271
+ textBgColor: init.style.textBgColor,
272
+ fontFamily: init.style.fontFamily,
273
+ fontSize: init.style.fontSize,
274
+ strokeStyle: init.style.strokeStyle,
275
+ lineWidth: init.style.lineWidth,
276
+ bottomOffset: init.style.bottomOffset
277
+ });
278
+ }
279
+ /** Worker 内重建 clip:只支持可序列化的素材类型 */
280
+ function createClipFromBuffer(buffer, kind, opts) {
281
+ switch (kind) {
282
+ case "video": return new MP4Clip(bufferToStream(buffer));
283
+ case "audio": return new AudioClip(bufferToStream(buffer));
284
+ case "image": return new ImgClip(bufferToStream(buffer));
285
+ case "subtitle": return createSubtitleClip({
286
+ srt: opts?.srt ?? "",
287
+ style: opts?.subtitleStyle ?? DEFAULT_SUBTITLE_STYLE,
288
+ width: opts?.width ?? 1280,
289
+ height: opts?.height ?? 720
290
+ });
291
+ case "text": return createTextClip({
292
+ text: opts?.text ?? "",
293
+ style: opts?.textStyle ?? DEFAULT_TEXT_STYLE,
294
+ width: opts?.width ?? 1280,
295
+ height: opts?.height ?? 720,
296
+ durationUs: opts?.durationUs ?? 5e6
297
+ });
298
+ default: throw new Error(`Worker 内不支持重建素材类型:${kind}`);
299
+ }
300
+ }
9
301
  // src/engine/webav/offset-clip.ts
10
302
  var OffsetClip = class OffsetClip {
11
303
  ready;
@@ -21,7 +313,10 @@ var OffsetClip = class OffsetClip {
21
313
  height: 0,
22
314
  duration: opts.durationUs
23
315
  };
24
- this.ready = inner.ready.then((meta) => {
316
+ this.ready = inner.ready.then(async (meta) => {
317
+ try {
318
+ await inner.tick(opts.inPointUs);
319
+ } catch {}
25
320
  this.metaValue = {
26
321
  width: meta.width,
27
322
  height: meta.height,
@@ -502,17 +797,23 @@ function createEditorRuntime() {
502
797
  }
503
798
  };
504
799
  }
800
+ // src/lib/error.ts
801
+ /**
802
+ * 异常 → 可展示文本。
803
+ *
804
+ * 各模块都会在 catch 里把 `unknown` 转成字符串,集中一处避免多份实现漂移。
805
+ */
806
+ function errorMessage(err) {
807
+ return err instanceof Error ? err.message : String(err);
808
+ }
505
809
  // src/editor/create-editor.ts
506
810
  function resolveBridge(bridge) {
507
811
  if (bridge != null) return bridge;
508
812
  return createWebBridge();
509
813
  }
510
814
  var loadImportFiles = () => Promise.resolve().then(() => import_files_exports);
511
- var loadImportPipeline = () => import("../chunks/import-pipeline-DLIdgrIb.js").then((n) => n.i);
815
+ var loadImportPipeline = () => Promise.resolve().then(() => import_pipeline_exports);
512
816
  var loadExportRunner = () => Promise.resolve().then(() => export_runner_exports);
513
- function errorMessage$1(err) {
514
- return err instanceof Error ? err.message : String(err);
515
- }
516
817
  /**
517
818
  * 创建编辑器实例。
518
819
  *
@@ -545,7 +846,10 @@ function createEditor(options = {}) {
545
846
  let disposed = false;
546
847
  let lastPlaying = false;
547
848
  let capabilityPromise = null;
849
+ /** 防抖计时器:最后一次变更后等待 intervalMs 再写盘 */
548
850
  let autoSaveTimer = null;
851
+ /** 串行化写盘:自动保存与手动保存永不并发(并发会交错写同一 namespace) */
852
+ let saveChain = Promise.resolve({ ok: true });
549
853
  let prevProject = store.getState().project;
550
854
  let prevAssets = store.getState().assets;
551
855
  let prevSelection = store.getState().selectedClipIds;
@@ -564,11 +868,20 @@ function createEditor(options = {}) {
564
868
  ns: namespace
565
869
  };
566
870
  }
871
+ /**
872
+ * 自动保存调度(防抖)。
873
+ *
874
+ * 语义是「最后一次结构性变更后静置 intervalMs 才写盘」,而不是
875
+ * 「第一次变更后固定 intervalMs 写盘」:
876
+ * 后者在持续编辑(拖滑块、连续裁剪)时会中途写一次,既浪费 I/O,
877
+ * 又会在宿主展示「保存中」时产生无意义的闪烁。每次新变更都重置计时器。
878
+ */
567
879
  function scheduleAutoSave() {
568
- if (autoSave == null || disposed || autoSaveTimer != null) return;
880
+ if (autoSave == null || disposed) return;
881
+ if (autoSaveTimer != null) clearTimeout(autoSaveTimer);
569
882
  autoSaveTimer = setTimeout(() => {
570
883
  autoSaveTimer = null;
571
- editor.save().catch((err) => {
884
+ enqueueSave().catch((err) => {
572
885
  bus.emit("error", {
573
886
  scope: "auto-save",
574
887
  error: err
@@ -576,6 +889,40 @@ function createEditor(options = {}) {
576
889
  });
577
890
  }, autoSave.intervalMs ?? 2e3);
578
891
  }
892
+ /** 把一次保存排进队列,返回该次保存的结果 */
893
+ function enqueueSave() {
894
+ const run = saveChain.then(() => writeProject());
895
+ saveChain = run.catch(() => ({ ok: false }));
896
+ return run;
897
+ }
898
+ /** 真正写盘 + 广播 project:saved */
899
+ async function writeProject() {
900
+ const { port, ns } = saveTarget();
901
+ const projectAtStart = store.getState().project;
902
+ const result = await port.save(ns, editor.serialize());
903
+ const changedDuringSave = store.getState().project !== projectAtStart;
904
+ if (result.ok) {
905
+ if (!changedDuringSave) store.getState().markSaved();
906
+ else scheduleAutoSave();
907
+ bus.emit("project:saved", result.path != null ? {
908
+ ok: true,
909
+ path: result.path
910
+ } : { ok: true });
911
+ } else bus.emit("project:saved", {
912
+ ok: false,
913
+ ...result.error != null ? { error: result.error } : {},
914
+ ...result.path != null ? { path: result.path } : {}
915
+ });
916
+ return result;
917
+ }
918
+ /** 卸载 / 关闭前把待写的防抖任务立刻执行 */
919
+ function flushAutoSave() {
920
+ if (autoSaveTimer != null) {
921
+ clearTimeout(autoSaveTimer);
922
+ autoSaveTimer = null;
923
+ }
924
+ return enqueueSave();
925
+ }
579
926
  const unsubscribeStore = store.subscribe((state) => {
580
927
  if (disposed) return;
581
928
  if (state.project !== prevProject) {
@@ -584,11 +931,11 @@ function createEditor(options = {}) {
584
931
  project: state.project,
585
932
  dirty: state.dirty
586
933
  });
934
+ if (autoSave != null) scheduleAutoSave();
587
935
  }
588
936
  if (state.dirty !== prevDirty) {
589
937
  prevDirty = state.dirty;
590
938
  bus.emit("project:dirty", { dirty: state.dirty });
591
- if (state.dirty && autoSave != null) scheduleAutoSave();
592
939
  }
593
940
  if (state.assets !== prevAssets) {
594
941
  prevAssets = state.assets;
@@ -694,8 +1041,10 @@ function createEditor(options = {}) {
694
1041
  if (file == null) continue;
695
1042
  try {
696
1043
  const result = await importFile(runtime.clips, file, void 0, { id: asset.id });
1044
+ const { regenerateDerived } = await loadImportPipeline();
1045
+ const withDerived = await regenerateDerived(runtime.clips, result.asset, file);
697
1046
  store.getState().replaceAsset(asset.id, {
698
- ...result.asset,
1047
+ ...withDerived,
699
1048
  id: asset.id
700
1049
  });
701
1050
  stillMissing.delete(asset.id);
@@ -717,20 +1066,7 @@ function createEditor(options = {}) {
717
1066
  return { missingAssetIds: remaining };
718
1067
  },
719
1068
  async save() {
720
- const { port, ns } = saveTarget();
721
- const result = await port.save(ns, editor.serialize());
722
- if (result.ok) {
723
- store.getState().markSaved();
724
- bus.emit("project:saved", result.path != null ? {
725
- ok: true,
726
- path: result.path
727
- } : { ok: true });
728
- } else bus.emit("project:saved", {
729
- ok: false,
730
- ...result.error != null ? { error: result.error } : {},
731
- ...result.path != null ? { path: result.path } : {}
732
- });
733
- return result;
1069
+ return enqueueSave();
734
1070
  },
735
1071
  async saveAs(nextStorage, nextNamespace) {
736
1072
  storage = nextStorage;
@@ -740,7 +1076,7 @@ function createEditor(options = {}) {
740
1076
  storage: nextStorage,
741
1077
  namespace: nextNamespace
742
1078
  };
743
- return editor.save();
1079
+ return enqueueSave();
744
1080
  },
745
1081
  isDirty: () => store.getState().dirty,
746
1082
  isEmpty: () => store.getState().project.clips.length === 0,
@@ -782,8 +1118,10 @@ function createEditor(options = {}) {
782
1118
  const { importFile } = await loadImportPipeline();
783
1119
  if (!store.getState().assets.some((a) => a.id === assetId)) throw new Error(`素材不存在:${assetId}`);
784
1120
  const result = await importFile(runtime.clips, file, void 0, { id: assetId });
1121
+ const { regenerateDerived } = await loadImportPipeline();
1122
+ const withDerived = await regenerateDerived(runtime.clips, result.asset, file);
785
1123
  store.getState().replaceAsset(assetId, {
786
- ...result.asset,
1124
+ ...withDerived,
787
1125
  id: assetId
788
1126
  });
789
1127
  store.getState().clearMissing(assetId);
@@ -895,7 +1233,7 @@ function createEditor(options = {}) {
895
1233
  } catch (err) {
896
1234
  if (cancelled || err instanceof ExportCancelledError) throw err instanceof ExportCancelledError ? err : new ExportCancelledError();
897
1235
  bus.emit("export:error", {
898
- message: errorMessage$1(err),
1236
+ message: errorMessage(err),
899
1237
  error: err
900
1238
  });
901
1239
  throw err;
@@ -915,7 +1253,7 @@ function createEditor(options = {}) {
915
1253
  } catch (err) {
916
1254
  return {
917
1255
  ok: false,
918
- error: errorMessage$1(err)
1256
+ error: errorMessage(err)
919
1257
  };
920
1258
  }
921
1259
  },
@@ -926,18 +1264,229 @@ function createEditor(options = {}) {
926
1264
  async dispose() {
927
1265
  if (disposed) return;
928
1266
  disposed = true;
929
- if (autoSaveTimer != null) {
930
- clearTimeout(autoSaveTimer);
931
- autoSaveTimer = null;
932
- }
1267
+ const pending = flushAutoSave();
933
1268
  unsubscribeStore();
934
1269
  bus.emit("dispose", {});
935
1270
  await runtime.dispose();
1271
+ try {
1272
+ await pending;
1273
+ } catch {}
936
1274
  bus.clear();
937
1275
  }
938
1276
  };
939
1277
  return editor;
940
1278
  }
1279
+ // src/engine/webav/import-pipeline.ts
1280
+ /**
1281
+ * 素材导入流水线(供 feature 与 Lab 共用)。
1282
+ *
1283
+ * 步骤:校验类型 → 创建母版 IClip → 读 meta → 抽缩略图 / 波形 → 写入 registry 与 store。
1284
+ *
1285
+ * ⚠️ 所有 WebAV 调用都落在 core 内;本文件位于 core/webav 下,允许 import '@webav/*'。
1286
+ */
1287
+ var import_pipeline_exports = /* @__PURE__ */ __exportAll({
1288
+ UnsupportedFileError: () => UnsupportedFileError,
1289
+ createSubtitleAsset: () => createSubtitleAsset,
1290
+ createTextAsset: () => createTextAsset,
1291
+ importFile: () => importFile,
1292
+ loadAudioClipForAsset: () => loadAudioClipForAsset,
1293
+ loadPcmForAsset: () => loadPcmForAsset,
1294
+ regenerateDerived: () => regenerateDerived
1295
+ });
1296
+ var THUMBNAIL_COUNT = 4;
1297
+ var THUMBNAIL_WIDTH = 120;
1298
+ var WAVEFORM_BUCKETS = 400;
1299
+ /**
1300
+ * 导入单个文件,生成素材记录。
1301
+ *
1302
+ * @param onProgress 0~1 进度(用于卡片骨架/进度条)
1303
+ * @throws UnsupportedFileError 类型不支持时
1304
+ */
1305
+ async function importFile(registry, file, onProgress, options = {}) {
1306
+ const detected = detectFileType(file);
1307
+ onProgress?.(.1);
1308
+ const { clip, meta } = await createClipFromFile(file, detected.kind);
1309
+ onProgress?.(.4);
1310
+ const assetId = options.id ?? createId("asset");
1311
+ const asset = {
1312
+ id: assetId,
1313
+ name: file.name,
1314
+ kind: detected.kind,
1315
+ mimeType: detected.mimeType,
1316
+ size: file.size,
1317
+ durationUs: asUs(meta.durationUs),
1318
+ width: meta.width,
1319
+ height: meta.height,
1320
+ hasAudio: meta.hasAudio,
1321
+ createdAt: Date.now()
1322
+ };
1323
+ if (detected.kind === "video") {
1324
+ try {
1325
+ asset.thumbnailUrls = (await generateThumbnails(clip, assetId, THUMBNAIL_COUNT, meta.durationUs, THUMBNAIL_WIDTH)).map((f) => f.url);
1326
+ } catch (err) {
1327
+ console.warn("[importFile] 缩略图生成失败", err);
1328
+ }
1329
+ onProgress?.(.7);
1330
+ }
1331
+ if (detected.kind === "audio" || detected.kind === "video" && meta.hasAudio) {
1332
+ try {
1333
+ if (detected.kind === "audio") {
1334
+ const pcm = clip.getPCMData();
1335
+ asset.waveform = computeWaveform(pcm, WAVEFORM_BUCKETS);
1336
+ }
1337
+ } catch (err) {
1338
+ console.warn("[importFile] 波形生成失败", err);
1339
+ }
1340
+ onProgress?.(.85);
1341
+ }
1342
+ registry.registerAsset(asset, file, clip);
1343
+ onProgress?.(1);
1344
+ return {
1345
+ asset,
1346
+ clipId: assetId,
1347
+ master: clip
1348
+ };
1349
+ }
1350
+ /**
1351
+ * 为一个「已有元数据」的素材重新生成派生数据(缩略图 / 波形)。
1352
+ *
1353
+ * 用于重新打开工程后 `resolveFile` / `reconnectAsset` 找回文件时:
1354
+ * 元数据里不含 thumbnailUrls(那是会话内 object URL,刷新即失效),
1355
+ * 如果不补这一步,素材卡片会一直显示图标占位 —— 用户看到的就是「资源坏了」。
1356
+ *
1357
+ * 失败只记警告:派生数据缺失不影响素材可用性。
1358
+ */
1359
+ async function regenerateDerived(registry, asset, file) {
1360
+ const master = registry.getMaster(asset.id);
1361
+ if (master == null) return asset;
1362
+ const next = { ...asset };
1363
+ if (asset.kind === "video") {
1364
+ const { regenerateThumbnails } = await import("../chunks/thumbnails-qH0dX_Zs.js").then((n) => n.r);
1365
+ try {
1366
+ next.thumbnailUrls = (await regenerateThumbnails(master, asset.id, THUMBNAIL_COUNT, asset.durationUs, THUMBNAIL_WIDTH)).map((f) => f.url);
1367
+ } catch (err) {
1368
+ console.warn("[regenerateDerived] 缩略图生成失败", asset.id, err);
1369
+ }
1370
+ }
1371
+ if (asset.kind === "audio" || asset.kind === "video" && asset.hasAudio) try {
1372
+ if (asset.kind === "audio") {
1373
+ const pcm = master.getPCMData();
1374
+ next.waveform = computeWaveform(pcm, WAVEFORM_BUCKETS);
1375
+ }
1376
+ } catch (err) {
1377
+ console.warn("[regenerateDerived] 波形生成失败", asset.id, err);
1378
+ }
1379
+ return next;
1380
+ }
1381
+ /** 新建文字素材(非文件来源,无需二进制) */
1382
+ function createTextAsset(params) {
1383
+ const assetId = params.id ?? createId("asset");
1384
+ const style = {
1385
+ ...DEFAULT_TEXT_STYLE,
1386
+ ...params.style
1387
+ };
1388
+ const durationUs = params.durationUs ?? 5e6;
1389
+ const master = createTextClip({
1390
+ text: params.text,
1391
+ style,
1392
+ width: params.width,
1393
+ height: params.height,
1394
+ durationUs
1395
+ });
1396
+ const asset = {
1397
+ id: assetId,
1398
+ name: params.text.slice(0, 24) || "文字",
1399
+ kind: "text",
1400
+ mimeType: "text/x-title",
1401
+ size: 0,
1402
+ durationUs: asUs(durationUs),
1403
+ width: params.width,
1404
+ height: params.height,
1405
+ hasAudio: false,
1406
+ createdAt: Date.now(),
1407
+ text: params.text,
1408
+ textStyle: style
1409
+ };
1410
+ params.registry.registerAsset(asset, null, master);
1411
+ return {
1412
+ asset,
1413
+ clipId: assetId
1414
+ };
1415
+ }
1416
+ /**
1417
+ * 解码素材的完整 PCM(48kHz 双声道),供 Worker 导出使用。
1418
+ *
1419
+ * ⚠️ 实测:Worker 内没有 AudioContext / OfflineAudioContext,
1420
+ * `new AudioClip(stream)` 会在 `decodeAudioData` 处抛错,因此必须在主线程解码后
1421
+ * 把 Float32Array 传过去(`new AudioClip(Float32Array[])` 走纯归一化分支)。
1422
+ *
1423
+ * @returns 声道数组;失败或素材无音轨时返回 undefined
1424
+ */
1425
+ async function loadPcmForAsset(registry, assetId) {
1426
+ if (registry.getAsset(assetId) == null) return void 0;
1427
+ const file = registry.getFile(assetId);
1428
+ if (file == null) return void 0;
1429
+ try {
1430
+ const clip = new AudioClip(fileToStream(file));
1431
+ await clip.ready;
1432
+ const pcm = clip.getPCMData().map((channel) => new Float32Array(channel));
1433
+ clip.destroy();
1434
+ return pcm;
1435
+ } catch (err) {
1436
+ console.warn("[loadPcmForAsset] 音频解码失败", err);
1437
+ return;
1438
+ }
1439
+ }
1440
+ /** 解析素材的音频 PCM(供预览音频引擎注册) */
1441
+ async function loadAudioClipForAsset(registry, assetId) {
1442
+ const file = registry.getFile(assetId);
1443
+ if (file == null) return null;
1444
+ const asset = registry.getAsset(assetId);
1445
+ if (asset == null) return null;
1446
+ if (asset.kind !== "audio" && asset.kind !== "video") return null;
1447
+ const clip = new AudioClip(fileToStream(file));
1448
+ await clip.ready;
1449
+ return clip;
1450
+ }
1451
+ /** 用第一条字幕文本给素材命名(无内容时回退为「字幕」) */
1452
+ function subtitleName(srt) {
1453
+ const base = (srt.split(/\r?\n/).map((line) => line.trim()).find((line) => line.length > 0 && !line.includes("-->") && !/^\d+$/.test(line)) ?? "") || "字幕";
1454
+ return base.length > 20 ? `${base.slice(0, 20)}…` : base;
1455
+ }
1456
+ /** 新建字幕素材(内容来自粘贴的 SRT,无源文件) */
1457
+ function createSubtitleAsset(params) {
1458
+ const assetId = params.id ?? createId("asset");
1459
+ const style = {
1460
+ ...DEFAULT_SUBTITLE_STYLE,
1461
+ ...params.style
1462
+ };
1463
+ const durationUs = params.durationUs ?? 5e6;
1464
+ const master = createSubtitleClip({
1465
+ srt: params.srt,
1466
+ style,
1467
+ width: params.width,
1468
+ height: params.height
1469
+ });
1470
+ const asset = {
1471
+ id: assetId,
1472
+ name: subtitleName(params.srt),
1473
+ kind: "subtitle",
1474
+ mimeType: "text/plain",
1475
+ size: params.srt.length,
1476
+ durationUs: asUs(durationUs),
1477
+ width: params.width,
1478
+ height: params.height,
1479
+ hasAudio: false,
1480
+ createdAt: Date.now(),
1481
+ srt: params.srt,
1482
+ subtitleStyle: style
1483
+ };
1484
+ params.registry.registerAsset(asset, null, master);
1485
+ return {
1486
+ asset,
1487
+ clipId: assetId
1488
+ };
1489
+ }
941
1490
  // src/engine/webav/import-files.ts
942
1491
  var import_files_exports = /* @__PURE__ */ __exportAll({ importFiles: () => importFiles });
943
1492
  /** 找出可以「重连」的素材 id(同名 + 同类型 + 当前确实缺少二进制) */
@@ -945,9 +1494,6 @@ function findReconnectableId(state, registry, file, kind) {
945
1494
  if (state.missingAssetIds.length === 0) return null;
946
1495
  return state.assets.find((asset) => state.missingAssetIds.includes(asset.id) && asset.kind === kind && asset.name === file.name && registry.getFile(asset.id) == null)?.id ?? null;
947
1496
  }
948
- function errorMessage(err) {
949
- return err instanceof Error ? err.message : String(err);
950
- }
951
1497
  /**
952
1498
  * 批量导入素材。
953
1499
  *
@@ -1189,11 +1735,12 @@ function mapClipToOutput(clip, source, target) {
1189
1735
  * @param instance 该 clip 独占的 IClip
1190
1736
  * @param clip 时间轴数据(提供 volume / fadeIn / fadeOut)
1191
1737
  */
1192
- function withClipGain(instance, clip) {
1193
- if (!(clip.muted || clip.volume !== 1 || clip.fadeInUs > 0 || clip.fadeOutUs > 0)) return instance;
1738
+ function withClipGain(instance, clip, options = {}) {
1739
+ const volume = options.trackMuted === true ? 0 : clip.volume;
1740
+ if (!(clip.muted || options.trackMuted === true || volume !== 1 || clip.fadeInUs > 0 || clip.fadeOutUs > 0)) return instance;
1194
1741
  if (clip.muted) return instance;
1195
1742
  return new GainClip(instance, {
1196
- volume: clip.volume,
1743
+ volume,
1197
1744
  fadeInUs: clip.fadeInUs,
1198
1745
  fadeOutUs: clip.fadeOutUs,
1199
1746
  durationUs: clip.durationUs,
@@ -1241,7 +1788,7 @@ async function buildCombinator(inputs, opts) {
1241
1788
  const { instance, track } = item;
1242
1789
  const clip = mapClipToOutput(item.clip, sourceSize, targetSize);
1243
1790
  if (track.kind === "audio" && !opts.includeAudio) continue;
1244
- if (track.muted) continue;
1791
+ const trackMuted = track.muted;
1245
1792
  if (clip.muted) continue;
1246
1793
  const duration = clampStillDuration(clip.durationUs);
1247
1794
  if (clip.durationUs <= 0) {
@@ -1255,7 +1802,7 @@ async function buildCombinator(inputs, opts) {
1255
1802
  ...clip,
1256
1803
  durationUs: duration
1257
1804
  };
1258
- const sprite = createOffscreenSprite(withClipGain(instance, normalized), normalized, { zIndex });
1805
+ const sprite = createOffscreenSprite(withClipGain(instance, normalized, { trackMuted }), normalized, { zIndex });
1259
1806
  applyOpacityFade(sprite, normalized);
1260
1807
  await combinator.addSprite(sprite, { main: false });
1261
1808
  maxTimeUs = Math.max(maxTimeUs, clip.startUs + duration);
@@ -1395,6 +1942,12 @@ async function buildWorkerPayload(registry, project, assets, settings, maxTimeUs
1395
1942
  includeAudio: settings.includeAudio,
1396
1943
  bgColor: "#000000",
1397
1944
  maxTimeUs,
1945
+ tracks: project.tracks.map((t) => ({
1946
+ id: t.id,
1947
+ kind: t.kind,
1948
+ hidden: t.hidden,
1949
+ muted: t.muted
1950
+ })),
1398
1951
  clips
1399
1952
  },
1400
1953
  missing
@@ -1669,6 +2222,7 @@ var SpriteSynchronizer = class {
1669
2222
  applyClipToSprite(existing, clip);
1670
2223
  existing.zIndex = zIndexOf(project.tracks, clip.trackId, clip);
1671
2224
  existing.visible = !track.hidden;
2225
+ if (previous == null || previous.durationUs !== clip.durationUs || previous.fadeInUs !== clip.fadeInUs || previous.fadeOutUs !== clip.fadeOutUs || previous.opacity !== clip.opacity) applyOpacityFade(existing, clip);
1672
2226
  this.lastClips.set(clip.id, clip);
1673
2227
  continue;
1674
2228
  }
@@ -1678,7 +2232,7 @@ var SpriteSynchronizer = class {
1678
2232
  this.registry.destroyInstance(clip.id);
1679
2233
  }
1680
2234
  try {
1681
- const instance = await this.registry.createInstance({
2235
+ const sprite = createVisibleSprite(await this.registry.createInstance({
1682
2236
  timelineClipId: clip.id,
1683
2237
  assetId: clip.assetId,
1684
2238
  inPointUs: clip.inPointUs,
@@ -1689,9 +2243,9 @@ var SpriteSynchronizer = class {
1689
2243
  subtitleStyle: clip.subtitleStyle,
1690
2244
  projectWidth: project.width,
1691
2245
  projectHeight: project.height
1692
- });
1693
- const sprite = createVisibleSprite(track.kind === "audio" ? instance : instance, clip, { zIndex: zIndexOf(project.tracks, clip.trackId, clip) });
2246
+ }), clip, { zIndex: zIndexOf(project.tracks, clip.trackId, clip) });
1694
2247
  sprite.visible = !track.hidden;
2248
+ applyOpacityFade(sprite, clip);
1695
2249
  if (track.kind === "audio" && asset.kind === "audio") {
1696
2250
  sprite.rect.w = 0;
1697
2251
  sprite.rect.h = 0;
@@ -2062,6 +2616,16 @@ var cancelled = false;
2062
2616
  function toStream(blob) {
2063
2617
  return blob.stream();
2064
2618
  }
2619
+ /** 轨道静音:音量置 0(画面不受影响) */
2620
+ function withSilence(clip) {
2621
+ return new GainClip(clip, {
2622
+ volume: 0,
2623
+ fadeInUs: 0,
2624
+ fadeOutUs: 0,
2625
+ durationUs: Number.MAX_SAFE_INTEGER,
2626
+ playbackRate: 1
2627
+ });
2628
+ }
2065
2629
  /** 音量 / 淡入淡出包装(与主线程 exporter.withClipGain 保持一致的语义) */
2066
2630
  function withGain(clip, timeline) {
2067
2631
  if (timeline.volume === 1 && timeline.fadeInUs === 0 && timeline.fadeOutUs === 0) return clip;
@@ -2124,6 +2688,7 @@ async function buildClipInWorker(asset) {
2124
2688
  */
2125
2689
  async function runExportInWorker(assets, timeline, onProgress, shouldCancel) {
2126
2690
  const assetMap = new Map(assets.map((a) => [a.id, a]));
2691
+ const trackMap = new Map((timeline.tracks ?? []).map((t) => [t.id, t]));
2127
2692
  const combinator = new Combinator({
2128
2693
  width: timeline.width,
2129
2694
  height: timeline.height,
@@ -2132,7 +2697,8 @@ async function runExportInWorker(assets, timeline, onProgress, shouldCancel) {
2132
2697
  bgColor: timeline.bgColor,
2133
2698
  audio: timeline.includeAudio ? void 0 : false
2134
2699
  });
2135
- const clipCache = /* @__PURE__ */ new Map();
2700
+ const masterCache = /* @__PURE__ */ new Map();
2701
+ const derived = [];
2136
2702
  const skipped = [];
2137
2703
  const sourceSize = {
2138
2704
  width: timeline.projectWidth ?? timeline.width,
@@ -2164,11 +2730,14 @@ async function runExportInWorker(assets, timeline, onProgress, shouldCancel) {
2164
2730
  });
2165
2731
  continue;
2166
2732
  }
2733
+ const track = trackMap.get(item.clip.trackId);
2734
+ if (track?.hidden === true) continue;
2167
2735
  if (item.clip.muted) continue;
2168
- let clip = clipCache.get(asset.id);
2169
- if (clip == null) try {
2170
- clip = await buildClipInWorker(asset);
2171
- clipCache.set(asset.id, clip);
2736
+ if (item.kind === "audio" && !timeline.includeAudio) continue;
2737
+ let master = masterCache.get(asset.id);
2738
+ if (master == null) try {
2739
+ master = await buildClipInWorker(asset);
2740
+ masterCache.set(asset.id, master);
2172
2741
  } catch (err) {
2173
2742
  const reason = err instanceof Error ? err.message : String(err);
2174
2743
  console.warn("[export.worker] clip skipped:", item.clip.id, reason);
@@ -2179,7 +2748,24 @@ async function runExportInWorker(assets, timeline, onProgress, shouldCancel) {
2179
2748
  continue;
2180
2749
  }
2181
2750
  const tc = item.clip;
2182
- const sprite = new OffscreenSprite(withGain(clip, tc));
2751
+ let instance;
2752
+ try {
2753
+ instance = await master.clone();
2754
+ } catch (err) {
2755
+ const reason = err instanceof Error ? err.message : String(err);
2756
+ skipped.push({
2757
+ clipId: tc.id,
2758
+ reason
2759
+ });
2760
+ continue;
2761
+ }
2762
+ if (tc.inPointUs > 0) instance = new OffsetClip(instance, {
2763
+ inPointUs: tc.inPointUs,
2764
+ durationUs: tc.durationUs * tc.playbackRate
2765
+ });
2766
+ derived.push(instance);
2767
+ const gainInput = track?.muted === true ? withSilence(instance) : instance;
2768
+ const sprite = new OffscreenSprite(withGain(gainInput, tc));
2183
2769
  const mapped = mapTransformToOutput(tc.transform, sourceSize, targetSize);
2184
2770
  sprite.rect.x = mapped.x;
2185
2771
  sprite.rect.y = mapped.y;
@@ -2194,6 +2780,7 @@ async function runExportInWorker(assets, timeline, onProgress, shouldCancel) {
2194
2780
  duration: tc.durationUs,
2195
2781
  playbackRate: tc.playbackRate
2196
2782
  };
2783
+ applyOpacityFade(sprite, tc);
2197
2784
  zIndex += 1;
2198
2785
  maxTimeUs = Math.max(maxTimeUs, tc.startUs + tc.durationUs);
2199
2786
  await combinator.addSprite(sprite, { main: false });
@@ -2215,10 +2802,14 @@ async function runExportInWorker(assets, timeline, onProgress, shouldCancel) {
2215
2802
  skipped
2216
2803
  };
2217
2804
  } finally {
2218
- for (const clip of clipCache.values()) try {
2805
+ for (const clip of derived) try {
2806
+ clip.destroy();
2807
+ } catch {}
2808
+ derived.length = 0;
2809
+ for (const clip of masterCache.values()) try {
2219
2810
  clip.destroy();
2220
2811
  } catch {}
2221
- clipCache.clear();
2812
+ masterCache.clear();
2222
2813
  try {
2223
2814
  combinator.destroy();
2224
2815
  } catch {}
@@ -2256,6 +2847,6 @@ function handleWorkerMessage(event) {
2256
2847
  });
2257
2848
  });
2258
2849
  }
2259
- export { ClipRegistry, DEFAULT_SUBTITLE_STYLE, DEFAULT_TEXT_STYLE, ENGINE_DEPENDENCIES_INSTALL_COMMAND, GainClip, OffsetClip, PcmClip, PlaybackClock, PreviewAudioEngine, PreviewCanvas, SpriteSynchronizer, TextClip, UnsupportedFileError, WorkerUnsupportedClipError, applyClipToSprite, applyOpacityFade, buildClipInWorker, buildCombinator, buildWorkerPayload, canUseWorker, checkExportSupport, computeExportDuration, computeTimelineEnd, createClipFromBuffer, createClipFromFile, createEditor, createEditorRuntime, createEditorStore, createEventBus, createHistoryStore, createOffscreenSprite, createSubtitleAsset, createSubtitleClip, createTextAsset, createTextClip, createVisibleSprite, cssFromStyle, detectDependencies, detectEnvironment, detectOpfs, exportProject, fixFMP4DurationStream, handleWorkerMessage, importFile, importFiles, isEnvironmentUsable, loadAudioClipForAsset, loadPcmForAsset, mapClipToOutput, mapTransformToOutput, runExportInWorker, selectAssetById, selectAssets, selectClipById, selectClips, selectPlayheadUs, selectProject, selectProjectDuration, selectSelectedClipIds, selectTracks, setDefaultExportWorkerFactory, startExport, summarizeCapabilities, withClipGain };
2850
+ export { ClipRegistry, DEFAULT_SUBTITLE_STYLE, DEFAULT_TEXT_STYLE, ENGINE_DEPENDENCIES_INSTALL_COMMAND, GainClip, OffsetClip, PcmClip, PlaybackClock, PreviewAudioEngine, PreviewCanvas, SpriteSynchronizer, TextClip, UnsupportedFileError, WorkerUnsupportedClipError, applyClipToSprite, applyOpacityFade, buildClipInWorker, buildCombinator, buildWorkerPayload, canUseWorker, checkExportSupport, computeExportDuration, computeTimelineEnd, createClipFromBuffer, createClipFromFile, createEditor, createEditorRuntime, createEditorStore, createEventBus, createHistoryStore, createOffscreenSprite, createSubtitleAsset, createSubtitleClip, createTextAsset, createTextClip, createVisibleSprite, cssFromStyle, detectDependencies, detectEnvironment, detectOpfs, exportProject, fixFMP4DurationStream, handleWorkerMessage, importFile, importFiles, isEnvironmentUsable, loadAudioClipForAsset, loadPcmForAsset, mapClipToOutput, mapTransformToOutput, regenerateDerived, runExportInWorker, selectAssetById, selectAssets, selectClipById, selectClips, selectPlayheadUs, selectProject, selectProjectDuration, selectSelectedClipIds, selectTracks, setDefaultExportWorkerFactory, startExport, summarizeCapabilities, withClipGain };
2260
2851
 
2261
2852
  //# sourceMappingURL=index.js.map