@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 +0,0 @@
1
- {"version":3,"file":"import-pipeline-DLIdgrIb.js","names":[],"sources":["../../src/engine/webav/styles.ts","../../src/engine/webav/text-clip.ts","../../src/engine/webav/clip-factory.ts","../../src/media/thumbnails.ts","../../src/engine/webav/import-pipeline.ts"],"sourcesContent":["/**\r\n * 文字 / 字幕的默认样式。抽成独立模块,避免 clip-factory 与 text-clip 相互依赖样式常量。\r\n */\r\n\r\nimport type { SubtitleStyle, TextStyle } from '../../types'\r\n\r\nexport const DEFAULT_TEXT_STYLE: TextStyle = {\r\n fontSize: 64,\r\n color: '#ffffff',\r\n fontFamily: 'sans-serif',\r\n fontWeight: 600,\r\n fontStyle: 'normal',\r\n strokeColor: '#000000',\r\n strokeWidth: 0,\r\n shadowColor: 'rgba(0,0,0,0.6)',\r\n shadowBlur: 8,\r\n align: 'center',\r\n background: 'transparent',\r\n}\r\n\r\n/**\r\n * 字幕默认字体。\r\n *\r\n * ⚠️ 不要写 'Noto Sans SC' —— 大多数系统并未安装该字体,`EmbedSubtitlesClip` 会把\r\n * `ctx.font` 设为 `30px Noto Sans SC`,此时浏览器回退到默认字体,中文可能显示为方框。\r\n * 使用系统字体栈(sans-serif)由浏览器挑选可用的中文字体,最稳。\r\n */\r\nexport const DEFAULT_SUBTITLE_STYLE: SubtitleStyle = {\r\n color: '#ffffff',\r\n textBgColor: null,\r\n fontFamily: 'sans-serif',\r\n fontSize: 30,\r\n strokeStyle: '#000000',\r\n lineWidth: null,\r\n bottomOffset: 30,\r\n}\r\n","/**\r\n * 自定义 IClip:把一段文字渲染成逐帧位图,实现动画标题。\r\n *\r\n * 之所以需要自研而不是直接用 ImgClip:\r\n * - ImgClip 的静态图 `meta.duration === Infinity`,Combinator 无法推断结束时间;\r\n * - 标题常常需要「打字机 / 闪烁」这类只跟时间有关的动画,用 tick 直接生成最直观。\r\n *\r\n * 单位约定:所有时间参数为微秒。\r\n */\r\n\r\nimport type { IClip } from '@webav/av-cliper'\r\nimport type { ClipMetaBase as IClipMeta } from './meta'\r\nimport { renderTxt2ImgBitmap } from '@webav/av-cliper'\r\nimport type { TextStyle } from '../../types'\r\nimport { DEFAULT_TEXT_STYLE } from './styles'\r\n\r\nexport type TextAnimation = 'none' | 'typewriter' | 'blink'\r\n\r\nexport interface TextClipOptions {\r\n text: string\r\n style: TextStyle\r\n width: number\r\n height: number\r\n durationUs: number\r\n animation?: TextAnimation\r\n /** 打字机动画每秒显示的字数 */\r\n charsPerSecond?: number\r\n /**\r\n * 动画相位偏移(微秒)。split 之后右半段需要从分割点继续动画,\r\n * 直接复用原 duration 会让动画重头播放。\r\n */\r\n animOffsetUs?: number\r\n}\r\n\r\nfunction cssFromStyle(style: TextStyle): string {\r\n const shadow = style.shadowBlur > 0\r\n ? `text-shadow: 0 2px ${style.shadowBlur}px ${style.shadowColor};`\r\n : ''\r\n const stroke = style.strokeWidth > 0\r\n ? `-webkit-text-stroke: ${style.strokeWidth}px ${style.strokeColor};`\r\n : ''\r\n return [\r\n `font-size:${style.fontSize}px`,\r\n `color:${style.color}`,\r\n `font-family:${style.fontFamily}`,\r\n `font-weight:${style.fontWeight}`,\r\n `font-style:${style.fontStyle}`,\r\n `text-align:${style.align}`,\r\n `background:${style.background}`,\r\n 'line-height:1.25',\r\n 'padding:8px 16px',\r\n 'white-space:pre-wrap',\r\n 'max-width:80%',\r\n stroke,\r\n shadow,\r\n ]\r\n .filter(Boolean)\r\n .join(';')\r\n}\r\n\r\n/**\r\n * 文字素材。内部缓存最近一次渲染的 ImageBitmap,按「文本 + 样式」做键避免重复渲染。\r\n */\r\nexport class TextClip implements IClip {\r\n readonly ready: Promise<IClipMeta>\r\n private opts: Required<Omit<TextClipOptions, 'animation' | 'charsPerSecond' | 'animOffsetUs'>> & {\r\n animation: TextAnimation\r\n charsPerSecond: number\r\n animOffsetUs: number\r\n }\r\n private metaValue: IClipMeta\r\n private cacheKey = ''\r\n private cachedBitmap: ImageBitmap | null = null\r\n private destroyed = false\r\n\r\n constructor(options: TextClipOptions) {\r\n this.opts = {\r\n ...options,\r\n style: { ...DEFAULT_TEXT_STYLE, ...options.style },\r\n animation: options.animation ?? 'none',\r\n charsPerSecond: options.charsPerSecond ?? 12,\r\n animOffsetUs: options.animOffsetUs ?? 0,\r\n }\r\n this.metaValue = {\r\n width: options.width,\r\n height: options.height,\r\n duration: Math.max(1, options.durationUs),\r\n }\r\n this.ready = Promise.resolve(this.metaValue)\r\n }\r\n\r\n get meta(): IClipMeta {\r\n return this.metaValue\r\n }\r\n\r\n private visibleTextAt(timeUs: number): string | null {\r\n const { text, animation, charsPerSecond } = this.opts\r\n if (animation === 'none') return text\r\n const phaseUs = timeUs + this.opts.animOffsetUs\r\n if (animation === 'typewriter') {\r\n const visible = Math.floor((phaseUs / 1_000_000) * charsPerSecond)\r\n if (visible <= 0) return null\r\n return text.slice(0, visible)\r\n }\r\n // blink:以 1Hz 闪烁\r\n const phase = Math.floor((phaseUs / 1_000_000) * 2) % 2\r\n return phase === 0 ? text : null\r\n }\r\n\r\n async tick(time: number): Promise<{\r\n video?: VideoFrame | ImageBitmap | null\r\n audio?: Float32Array[]\r\n state: 'done' | 'success'\r\n }> {\r\n if (this.destroyed) return { state: 'done' }\r\n if (time >= this.opts.durationUs) return { state: 'done' }\r\n\r\n const visible = this.visibleTextAt(time)\r\n if (visible == null || visible.length === 0) {\r\n // 空文本时返回一张透明的同尺寸位图,避免 sprite 保留上一帧\r\n const bitmap = await this.renderEmpty()\r\n return { video: bitmap, state: 'success' }\r\n }\r\n\r\n const key = `${visible}|${JSON.stringify(this.opts.style)}`\r\n if (key !== this.cacheKey || this.cachedBitmap == null) {\r\n this.cachedBitmap?.close()\r\n this.cachedBitmap = await this.renderTextToFrame(visible)\r\n this.cacheKey = key\r\n }\r\n // ⚠️ 必须返回副本:Sprite 持有上一帧时会 close 掉它(实测 OffscreenSprite/VisibleSprite\r\n // 都会 `prev?.close()`),直接返回缓存位图会被关闭,下一帧读像素直接抛错。\r\n return { video: await createImageBitmap(this.cachedBitmap), state: 'success' }\r\n }\r\n\r\n /**\r\n * 把文字渲染成「整帧」位图(工程分辨率)。\r\n *\r\n * 为什么不能直接把 renderTxt2ImgBitmap 的结果当帧用:\r\n * 它返回的是文字自身尺寸的小图(几十到几百像素宽),Sprite 会把它拉伸到 rect 的\r\n * 1280×720 —— 文字被放大后严重模糊,且居中位置由 Sprite 决定而非文字块决定。\r\n * 因此这里先渲染文字小图,再按比例居中合成到工程尺寸的画布上。\r\n */\r\n private async renderTextToFrame(text: string): Promise<ImageBitmap> {\r\n const { width, height } = this.opts\r\n const canvas = new OffscreenCanvas(width, height)\r\n const ctx = canvas.getContext('2d')\r\n if (ctx == null) throw new Error('无法创建文字渲染上下文')\r\n\r\n const layer = await renderTxt2ImgBitmap(text, cssFromStyle(this.opts.style))\r\n const align = this.opts.style.align\r\n const margin = Math.round(width * 0.05)\r\n // 文字超宽时等比缩小,保证完整可见\r\n const maxWidth = width - margin * 2\r\n const scale = layer.width > maxWidth ? maxWidth / layer.width : 1\r\n const drawW = Math.round(layer.width * scale)\r\n const drawH = Math.round(layer.height * scale)\r\n const x =\r\n align === 'left' ? margin : align === 'right' ? width - margin - drawW : (width - drawW) / 2\r\n const y = Math.round((height - drawH) / 2)\r\n\r\n ctx.drawImage(layer, x, y, drawW, drawH)\r\n layer.close()\r\n return canvas.transferToImageBitmap()\r\n }\r\n\r\n private emptyBitmap: ImageBitmap | null = null\r\n\r\n private async renderEmpty(): Promise<ImageBitmap> {\r\n if (this.emptyBitmap != null) return this.emptyBitmap\r\n const canvas = new OffscreenCanvas(this.opts.width, this.opts.height)\r\n this.emptyBitmap = canvas.transferToImageBitmap()\r\n return this.emptyBitmap\r\n }\r\n\r\n async split(time: number): Promise<[this, this]> {\r\n if (time <= 0 || time >= this.opts.durationUs) {\r\n throw new Error('\"time\" out of bounds')\r\n }\r\n const left = new TextClip({ ...this.opts, durationUs: time })\r\n const right = new TextClip({\r\n ...this.opts,\r\n durationUs: this.opts.durationUs - time,\r\n // 右半段继承动画相位,避免打字机动画从 0 重新开始\r\n animOffsetUs: this.opts.animOffsetUs + time,\r\n })\r\n await Promise.all([left.ready, right.ready])\r\n return [left as this, right as this]\r\n }\r\n\r\n async clone(): Promise<this> {\r\n const copy = new TextClip({ ...this.opts })\r\n await copy.ready\r\n return copy as this\r\n }\r\n\r\n destroy(): void {\r\n this.destroyed = true\r\n this.cachedBitmap?.close()\r\n this.cachedBitmap = null\r\n this.emptyBitmap?.close()\r\n this.emptyBitmap = null\r\n }\r\n}\r\n\r\nexport { cssFromStyle }\r\n","/**\r\n * File → IClip 的分派工厂。\r\n *\r\n * ⚠️ 本文件是 `@webav/*` 的合法引用点之一(另一个是 core/webav 下的其他模块)。\r\n * 不要在 features / store / components 中直接 import WebAV。\r\n *\r\n * 所有 clip 实例的生命周期由 `ClipRegistry` 统一管理,这里只负责创建。\r\n */\r\n\r\nimport {\r\n AudioClip,\r\n EmbedSubtitlesClip,\r\n ImgClip,\r\n MP4Clip,\r\n} from '@webav/av-cliper'\r\nimport type { IClip } from '@webav/av-cliper'\r\nimport type { MediaAsset, SubtitleStyle, TextStyle } from '../../types'\r\nimport type { MP4ClipMeta } from './meta'\r\nimport { bufferToStream, fileToStream } from '../../media/file-reader'\r\nimport { DEFAULT_STILL_DURATION_US } from '../../constants'\r\nimport { TextClip } from './text-clip'\r\nimport { DEFAULT_SUBTITLE_STYLE, DEFAULT_TEXT_STYLE } from './styles'\r\n\r\nexport interface AssetMetaSnapshot {\r\n durationUs: number\r\n width: number\r\n height: number\r\n hasAudio: boolean\r\n}\r\n\r\n/**\r\n * 创建素材的「母版」clip 并读取 meta。\r\n *\r\n * 对静态图/文字/字幕,`meta.duration` 可能是 Infinity,这里会归一成有限值:\r\n * 真正的时长由时间轴 clip 决定,母版只用于 clone 与抽帧。\r\n */\r\nexport async function createClipFromFile(\r\n file: File,\r\n kind: MediaAsset['kind'],\r\n): Promise<{ clip: IClip; meta: AssetMetaSnapshot }> {\r\n switch (kind) {\r\n case 'video': {\r\n const clip = new MP4Clip(fileToStream(file))\r\n const meta = (await clip.ready) as MP4ClipMeta\r\n return {\r\n clip,\r\n meta: {\r\n durationUs: meta.duration,\r\n width: meta.width,\r\n height: meta.height,\r\n hasAudio: meta.audioChanCount > 0,\r\n },\r\n }\r\n }\r\n case 'audio': {\r\n const clip = new AudioClip(fileToStream(file))\r\n const meta = await clip.ready\r\n return {\r\n clip,\r\n meta: {\r\n durationUs: meta.duration,\r\n width: 0,\r\n height: 0,\r\n hasAudio: true,\r\n },\r\n }\r\n }\r\n case 'image': {\r\n const clip = new ImgClip(fileToStream(file))\r\n const meta = await clip.ready\r\n return {\r\n clip,\r\n meta: {\r\n durationUs: Number.isFinite(meta.duration) ? meta.duration : DEFAULT_STILL_DURATION_US,\r\n width: meta.width,\r\n height: meta.height,\r\n hasAudio: false,\r\n },\r\n }\r\n }\r\n case 'subtitle': {\r\n const text = await file.text()\r\n const clip = new EmbedSubtitlesClip(text, {\r\n videoWidth: 1280,\r\n videoHeight: 720,\r\n })\r\n const meta = await clip.ready\r\n return {\r\n clip,\r\n meta: {\r\n durationUs: Number.isFinite(meta.duration) ? meta.duration : DEFAULT_STILL_DURATION_US,\r\n width: meta.width,\r\n height: meta.height,\r\n hasAudio: false,\r\n },\r\n }\r\n }\r\n default:\r\n throw new Error(`Unsupported asset kind: ${kind}`)\r\n }\r\n}\r\n\r\nexport interface TextClipInit {\r\n text: string\r\n style: TextStyle\r\n width: number\r\n height: number\r\n durationUs: number\r\n}\r\n\r\n/** 文字素材:不走文件,直接由 TextClip 生成 */\r\nexport function createTextClip(init: TextClipInit): IClip {\r\n return new TextClip({\r\n text: init.text,\r\n style: init.style,\r\n width: init.width,\r\n height: init.height,\r\n durationUs: init.durationUs,\r\n })\r\n}\r\n\r\nexport interface SubtitleClipInit {\r\n srt: string\r\n style: SubtitleStyle\r\n width: number\r\n height: number\r\n}\r\n\r\nexport function createSubtitleClip(init: SubtitleClipInit): IClip {\r\n return new EmbedSubtitlesClip(init.srt, {\r\n videoWidth: init.width,\r\n videoHeight: init.height,\r\n color: init.style.color,\r\n textBgColor: init.style.textBgColor,\r\n fontFamily: init.style.fontFamily,\r\n fontSize: init.style.fontSize,\r\n strokeStyle: init.style.strokeStyle,\r\n lineWidth: init.style.lineWidth,\r\n bottomOffset: init.style.bottomOffset,\r\n })\r\n}\r\n\r\n/** Worker 内重建 clip:只支持可序列化的素材类型 */\r\nexport function createClipFromBuffer(\r\n buffer: ArrayBuffer,\r\n kind: MediaAsset['kind'],\r\n opts?: {\r\n width?: number\r\n height?: number\r\n text?: string\r\n textStyle?: TextStyle\r\n srt?: string\r\n subtitleStyle?: SubtitleStyle\r\n durationUs?: number\r\n },\r\n): IClip {\r\n switch (kind) {\r\n case 'video':\r\n return new MP4Clip(bufferToStream(buffer))\r\n case 'audio':\r\n return new AudioClip(bufferToStream(buffer))\r\n case 'image':\r\n return new ImgClip(bufferToStream(buffer))\r\n case 'subtitle':\r\n return createSubtitleClip({\r\n srt: opts?.srt ?? '',\r\n style: opts?.subtitleStyle ?? DEFAULT_SUBTITLE_STYLE,\r\n width: opts?.width ?? 1280,\r\n height: opts?.height ?? 720,\r\n })\r\n case 'text':\r\n return createTextClip({\r\n text: opts?.text ?? '',\r\n style: opts?.textStyle ?? DEFAULT_TEXT_STYLE,\r\n width: opts?.width ?? 1280,\r\n height: opts?.height ?? 720,\r\n durationUs: opts?.durationUs ?? DEFAULT_STILL_DURATION_US,\r\n })\r\n default:\r\n throw new Error(`Worker 内不支持重建素材类型:${kind}`)\r\n }\r\n}\r\n","/**\r\n * 缩略图生成。`MP4Clip.thumbnails()` 封装 + 内存缓存。\r\n *\r\n * 注意:`thumbnails()` 默认每个关键帧一张;调用方若只要 N 张,应传 `step`。\r\n * 返回值是 Blob,转成 object URL 后要记得在素材移除时 revoke。\r\n */\r\n\r\nimport type { Us } from '../timeline/time'\r\nimport { usToMs } from '../timeline/time'\r\n\r\nexport interface ThumbnailFrame {\r\n ts: number\r\n url: string\r\n}\r\n\r\nconst cache = new Map<string, ThumbnailFrame[]>()\r\n\r\n/** 缩略图抽帧所需的最小接口(避免 core/media 直接依赖 @webav) */\r\nexport interface ThumbnailSource {\r\n thumbnails(\r\n imgWidth?: number,\r\n opts?: { start?: number; end?: number; step?: number },\r\n ): Promise<Array<{ ts: number; img: Blob }>>\r\n}\r\n\r\n/**\r\n * 生成等间隔缩略图条。\r\n *\r\n * @param clip MP4Clip 实例(调用方负责生命周期)\r\n * @param assetId 缓存 key\r\n * @param count 期望张数\r\n * @param durationUs 素材总时长,用来算 step\r\n * @param width 单张宽度\r\n */\r\nexport async function generateThumbnails(\r\n clip: ThumbnailSource,\r\n assetId: string,\r\n count: number,\r\n durationUs: number,\r\n width = 100,\r\n): Promise<ThumbnailFrame[]> {\r\n const cached = cache.get(assetId)\r\n if (cached) return cached\r\n\r\n const stepUs = Math.max(Math.floor(durationUs / count), usToMs(200))\r\n const raw = await clip.thumbnails(width, {\r\n start: 0,\r\n end: Math.max(0, durationUs - 1),\r\n step: stepUs,\r\n })\r\n\r\n // step 模式可能产生远多于 count 的张数(步长被内部按关键帧吸附)。\r\n // 这里做一次等距抽取,保证最多 count 张。\r\n const picked: Array<{ ts: number; img: Blob }> = []\r\n if (raw.length <= count) {\r\n picked.push(...raw)\r\n } else {\r\n const stride = raw.length / count\r\n for (let i = 0; i < count; i += 1) {\r\n const item = raw[Math.min(raw.length - 1, Math.floor(i * stride))]\r\n if (item) picked.push(item)\r\n }\r\n }\r\n\r\n const frames = picked.map((item) => ({\r\n ts: item.ts,\r\n url: URL.createObjectURL(item.img),\r\n }))\r\n cache.set(assetId, frames)\r\n return frames\r\n}\r\n\r\n/** 释放某素材的缩略图 URL */\r\nexport function releaseThumbnails(assetId: string): void {\r\n const frames = cache.get(assetId)\r\n if (!frames) return\r\n for (const f of frames) URL.revokeObjectURL(f.url)\r\n cache.delete(assetId)\r\n}\r\n\r\n/** 抽帧结果转成时间戳列表(Lab 使用) */\r\nexport function toTimestampList(frames: Array<{ ts: number }>): Us[] {\r\n return frames.map((f) => f.ts as Us)\r\n}\r\n","/**\r\n * 素材导入流水线(供 feature 与 Lab 共用)。\r\n *\r\n * 步骤:校验类型 → 创建母版 IClip → 读 meta → 抽缩略图 / 波形 → 写入 registry 与 store。\r\n *\r\n * ⚠️ 所有 WebAV 调用都落在 core 内;本文件位于 core/webav 下,允许 import '@webav/*'。\r\n */\r\n\r\nimport { AudioClip } from '@webav/av-cliper'\r\nimport type { IClip } from '@webav/av-cliper'\r\nimport type { MP4Clip } from '@webav/av-cliper'\r\nimport type { MediaAsset, TextStyle } from '../../types'\r\nimport { detectFileType, fileToStream, UnsupportedFileError } from '../../media/file-reader'\r\nimport { generateThumbnails } from '../../media/thumbnails'\r\nimport { computeWaveform } from '../../media/waveform'\r\nimport { DEFAULT_STILL_DURATION_US } from '../../constants'\r\nimport { asUs } from '../../timeline/time'\r\nimport { createId } from '../../lib/id'\r\nimport { createClipFromFile, createSubtitleClip, createTextClip } from './clip-factory'\r\nimport { DEFAULT_SUBTITLE_STYLE, DEFAULT_TEXT_STYLE } from './styles'\r\nimport type { ClipRegistry } from './clip-registry'\r\n\r\nexport interface ImportedAsset {\r\n asset: MediaAsset\r\n clipId: string\r\n /** 母版 clip(重连素材时用于替换旧的母版) */\r\n master?: IClip\r\n}\r\n\r\nconst THUMBNAIL_COUNT = 4\r\nconst THUMBNAIL_WIDTH = 120\r\nconst WAVEFORM_BUCKETS = 400\r\n\r\nexport interface ImportFileOptions {\r\n /**\r\n * 指定素材 id(宿主把自己的素材记录 id 作为 `MediaAsset.id`)。\r\n *\r\n * 传了就必须保证会话内唯一:重复注册会覆盖 registry 里的旧素材。\r\n */\r\n id?: string\r\n}\r\n\r\n/**\r\n * 导入单个文件,生成素材记录。\r\n *\r\n * @param onProgress 0~1 进度(用于卡片骨架/进度条)\r\n * @throws UnsupportedFileError 类型不支持时\r\n */\r\nexport async function importFile(\r\n registry: ClipRegistry,\r\n file: File,\r\n onProgress?: (progress: number) => void,\r\n options: ImportFileOptions = {},\r\n): Promise<ImportedAsset> {\r\n const detected = detectFileType(file)\r\n onProgress?.(0.1)\r\n\r\n const { clip, meta } = await createClipFromFile(file, detected.kind)\r\n onProgress?.(0.4)\r\n\r\n const assetId = options.id ?? createId('asset')\r\n const asset: MediaAsset = {\r\n id: assetId,\r\n name: file.name,\r\n kind: detected.kind,\r\n mimeType: detected.mimeType,\r\n size: file.size,\r\n durationUs: asUs(meta.durationUs),\r\n width: meta.width,\r\n height: meta.height,\r\n hasAudio: meta.hasAudio,\r\n createdAt: Date.now(),\r\n }\r\n\r\n if (detected.kind === 'video') {\r\n try {\r\n const frames = await generateThumbnails(\r\n clip as MP4Clip,\r\n assetId,\r\n THUMBNAIL_COUNT,\r\n meta.durationUs,\r\n THUMBNAIL_WIDTH,\r\n )\r\n asset.thumbnailUrls = frames.map((f) => f.url)\r\n } catch (err) {\r\n // 抽帧失败不应阻断导入:给出警告但保留素材\r\n console.warn('[importFile] 缩略图生成失败', err)\r\n }\r\n onProgress?.(0.7)\r\n }\r\n\r\n if (detected.kind === 'audio' || (detected.kind === 'video' && meta.hasAudio)) {\r\n try {\r\n if (detected.kind === 'audio') {\r\n const pcm = (clip as AudioClip).getPCMData()\r\n asset.waveform = computeWaveform(pcm, WAVEFORM_BUCKETS)\r\n }\r\n } catch (err) {\r\n console.warn('[importFile] 波形生成失败', err)\r\n }\r\n onProgress?.(0.85)\r\n }\r\n\r\n registry.registerAsset(asset, file, clip)\r\n onProgress?.(1)\r\n return { asset, clipId: assetId, master: clip }\r\n}\r\n\r\n/** 新建文字素材(非文件来源,无需二进制) */\r\nexport function createTextAsset(params: {\r\n registry: ClipRegistry\r\n text: string\r\n style?: Partial<TextStyle>\r\n width: number\r\n height: number\r\n durationUs?: number\r\n /** 指定素材 id;不传则生成 `asset_xxx` */\r\n id?: string\r\n}): ImportedAsset {\r\n const assetId = params.id ?? createId('asset')\r\n const style: TextStyle = { ...DEFAULT_TEXT_STYLE, ...params.style }\r\n const durationUs = params.durationUs ?? DEFAULT_STILL_DURATION_US\r\n\r\n const master = createTextClip({\r\n text: params.text,\r\n style,\r\n width: params.width,\r\n height: params.height,\r\n durationUs,\r\n })\r\n\r\n const asset: MediaAsset = {\r\n id: assetId,\r\n name: params.text.slice(0, 24) || '文字',\r\n kind: 'text',\r\n mimeType: 'text/x-title',\r\n size: 0,\r\n durationUs: asUs(durationUs),\r\n width: params.width,\r\n height: params.height,\r\n hasAudio: false,\r\n createdAt: Date.now(),\r\n text: params.text,\r\n textStyle: style,\r\n }\r\n\r\n params.registry.registerAsset(asset, null, master)\r\n return { asset, clipId: assetId }\r\n}\r\n\r\n/**\r\n * 解码素材的完整 PCM(48kHz 双声道),供 Worker 导出使用。\r\n *\r\n * ⚠️ 实测:Worker 内没有 AudioContext / OfflineAudioContext,\r\n * `new AudioClip(stream)` 会在 `decodeAudioData` 处抛错,因此必须在主线程解码后\r\n * 把 Float32Array 传过去(`new AudioClip(Float32Array[])` 走纯归一化分支)。\r\n *\r\n * @returns 声道数组;失败或素材无音轨时返回 undefined\r\n */\r\nexport async function loadPcmForAsset(\r\n registry: ClipRegistry,\r\n assetId: string,\r\n): Promise<Float32Array[] | undefined> {\r\n const asset = registry.getAsset(assetId)\r\n if (asset == null) return undefined\r\n const file = registry.getFile(assetId)\r\n if (file == null) return undefined\r\n try {\r\n // 视频素材也能用 decodeAudioData 解析(内部解码 AAC 音轨)\r\n const clip = new AudioClip(fileToStream(file))\r\n await clip.ready\r\n const pcm = clip.getPCMData().map((channel) => new Float32Array(channel))\r\n clip.destroy()\r\n return pcm\r\n } catch (err) {\r\n console.warn('[loadPcmForAsset] 音频解码失败', err)\r\n return undefined\r\n }\r\n}\r\n\r\n/** 解析素材的音频 PCM(供预览音频引擎注册) */\r\nexport async function loadAudioClipForAsset(\r\n registry: ClipRegistry,\r\n assetId: string,\r\n): Promise<AudioClip | null> {\r\n const file = registry.getFile(assetId)\r\n if (file == null) return null\r\n const asset = registry.getAsset(assetId)\r\n if (asset == null) return null\r\n if (asset.kind !== 'audio' && asset.kind !== 'video') return null\r\n const clip = new AudioClip(fileToStream(file))\r\n await clip.ready\r\n return clip\r\n}\r\n\r\n/** 用第一条字幕文本给素材命名(无内容时回退为「字幕」) */\r\nfunction subtitleName(srt: string): string {\r\n const firstLine =\r\n srt\r\n .split(/\\r?\\n/)\r\n .map((line) => line.trim())\r\n .find((line) => line.length > 0 && !line.includes('-->') && !/^\\d+$/.test(line)) ?? ''\r\n const base = firstLine || '字幕'\r\n return base.length > 20 ? `${base.slice(0, 20)}…` : base\r\n}\r\n\r\n/** 新建字幕素材(内容来自粘贴的 SRT,无源文件) */\r\nexport function createSubtitleAsset(params: {\r\n registry: ClipRegistry\r\n srt: string\r\n width: number\r\n height: number\r\n durationUs?: number\r\n style?: Partial<import('../../types').SubtitleStyle>\r\n /** 指定素材 id;不传则生成 `asset_xxx` */\r\n id?: string\r\n}): ImportedAsset {\r\n const assetId = params.id ?? createId('asset')\r\n const style = { ...DEFAULT_SUBTITLE_STYLE, ...params.style }\r\n const durationUs = params.durationUs ?? DEFAULT_STILL_DURATION_US\r\n\r\n const master = createSubtitleClip({\r\n srt: params.srt,\r\n style,\r\n width: params.width,\r\n height: params.height,\r\n })\r\n\r\n const asset: MediaAsset = {\r\n id: assetId,\r\n name: subtitleName(params.srt),\r\n kind: 'subtitle',\r\n mimeType: 'text/plain',\r\n size: params.srt.length,\r\n durationUs: asUs(durationUs),\r\n width: params.width,\r\n height: params.height,\r\n hasAudio: false,\r\n createdAt: Date.now(),\r\n srt: params.srt,\r\n subtitleStyle: style,\r\n }\r\n\r\n params.registry.registerAsset(asset, null, master)\r\n return { asset, clipId: assetId }\r\n}\r\n\r\nexport { UnsupportedFileError }\r\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"time-C_gvkVl1.js","names":[],"sources":["../../src/constants.ts","../../src/lib/id.ts","../../src/timeline/time.ts"],"sourcesContent":["/**\n * cut-core 自有常量。\n *\n * 只放「逻辑层必须知道」的数值:默认时长、默认音量、工程 schema 版本、\n * 历史栈深度。UI 相关的量(缩放档位、面板高度、吸附像素阈值)留在宿主侧,\n * 避免组件库被时间轴界面的调参牵着走。\n */\n\n/** 图片 / 文字素材默认时长(微秒) */\nexport const DEFAULT_STILL_DURATION_US = 5_000_000\n\n/** 默认音量,避免多轨直接相加削波 */\nexport const DEFAULT_CLIP_VOLUME = 0.8\n\n/** 工程 schema 版本 */\nexport const PROJECT_SCHEMA_VERSION = 1\n\n/** 撤销栈最大深度 */\nexport const MAX_HISTORY_STEPS = 50\n\n/**\n * 连续同质操作的合并时间窗口(毫秒)。\n *\n * 拖动滑块 / 画布内拖拽素材时会连续触发大量更新;窗口内相同 mergeKey 的记录\n * 会被合并成一步撤销。取值兼顾「连续拖动算一次」与「停顿后再改算新一步」。\n */\nexport const HISTORY_MERGE_WINDOW_MS = 800\n","import { nanoid } from 'nanoid'\n\n/**\n * 生成短 id。scope 仅用于可读性(例如 `clip_xxx`),不影响唯一性。\n */\nexport function createId(scope?: string): string {\n const id = nanoid(10)\n return scope ? `${scope}_${id}` : id\n}\n","/**\n * 时间与像素的单位换算。\n *\n * 内部统一使用微秒(µs)。为避免中途混用毫秒/秒,这里定义 branded type:\n * - `Us` 微秒\n * - `Ms` 毫秒\n * - `Px` 像素\n *\n * 只在函数边界处做 cast,业务代码不允许出现裸 number 参与时间计算。\n */\n\ndeclare const usBrand: unique symbol\ndeclare const msBrand: unique symbol\ndeclare const pxBrand: unique symbol\n\nexport type Us = number & { readonly [usBrand]: true }\nexport type Ms = number & { readonly [msBrand]: true }\nexport type Px = number & { readonly [pxBrand]: true }\n\nexport const US_PER_SECOND = 1_000_000\nexport const US_PER_MS = 1_000\n\n/** 秒 → 微秒 */\nexport function secToUs(sec: number): Us {\n return Math.round(sec * US_PER_SECOND) as Us\n}\n\n/** 微秒 → 秒 */\nexport function usToSec(us: number): number {\n return us / US_PER_SECOND\n}\n\n/** 毫秒 → 微秒 */\nexport function msToUs(ms: number): Us {\n return Math.round(ms * US_PER_MS) as Us\n}\n\n/** 微秒 → 毫秒 */\nexport function usToMs(us: number): Ms {\n return Math.round(us / US_PER_MS) as Ms\n}\n\n/** 数字 → 微秒(用于从 DOM/WebAV 回调拿到的裸数字) */\nexport function asUs(n: number): Us {\n return Math.round(n) as Us\n}\n\n/** 数字 → 像素 */\nexport function asPx(n: number): Px {\n return n as Px\n}\n\n/**\n * 微秒 → 像素。\n * @param us 时间(微秒)\n * @param pxPerSecond 每秒对应多少像素\n */\nexport function usToPx(us: number, pxPerSecond: number): Px {\n return ((us / US_PER_SECOND) * pxPerSecond) as Px\n}\n\n/**\n * 像素 → 微秒。\n * @param px 像素\n * @param pxPerSecond 每秒对应多少像素\n */\nexport function pxToUs(px: number, pxPerSecond: number): Us {\n return Math.round((px / pxPerSecond) * US_PER_SECOND) as Us\n}\n\n/** 微秒 → `HH:MM:SS.mmm` */\nexport function formatTimecode(us: number, showMs = true): string {\n const safe = Math.max(0, us)\n const totalMs = Math.floor(safe / 1000)\n const ms = totalMs % 1000\n const totalSec = Math.floor(totalMs / 1000)\n const sec = totalSec % 60\n const min = Math.floor(totalSec / 60) % 60\n const hour = Math.floor(totalSec / 3600)\n const pad = (n: number, len = 2) => String(n).padStart(len, '0')\n const base = `${pad(hour)}:${pad(min)}:${pad(sec)}`\n return showMs ? `${base}.${pad(ms, 3)}` : base\n}\n\n/** 微秒 → `MM:SS` 或 `HH:MM:SS`,列表展示用 */\nexport function formatDuration(us: number): string {\n if (!Number.isFinite(us)) return '∞'\n const totalSec = Math.max(0, Math.round(us / US_PER_SECOND))\n const sec = totalSec % 60\n const min = Math.floor(totalSec / 60) % 60\n const hour = Math.floor(totalSec / 3600)\n const pad = (n: number) => String(n).padStart(2, '0')\n return hour > 0 ? `${hour}:${pad(min)}:${pad(sec)}` : `${pad(min)}:${pad(sec)}`\n}\n\nexport interface RulerTick {\n us: Us\n px: Px\n major: boolean\n}\n\nconst TICK_STEPS_US = [\n 100_000, 200_000, 500_000, 1_000_000, 2_000_000, 5_000_000, 10_000_000, 15_000_000,\n 30_000_000, 60_000_000, 120_000_000, 300_000_000, 600_000_000,\n]\n\n/**\n * 根据缩放级别生成刻度尺。\n *\n * @param pxPerSecond 每秒像素\n * @param viewportWidthPx 可视宽度(像素)\n * @param scrollLeftPx 横向滚动偏移(像素)\n * @returns 主刻度(带文字)与次刻度\n */\nexport function buildTicks(\n pxPerSecond: number,\n viewportWidthPx: number,\n scrollLeftPx = 0,\n): RulerTick[] {\n const startUs = pxToUs(scrollLeftPx, pxPerSecond)\n const endUs = pxToUs(scrollLeftPx + viewportWidthPx, pxPerSecond)\n const minLabelSpacingPx = 72\n const step =\n TICK_STEPS_US.find((s) => usToPx(s, pxPerSecond) >= minLabelSpacingPx) ??\n TICK_STEPS_US[TICK_STEPS_US.length - 1]\n const minorStep = step / 5\n const first = Math.floor(startUs / step) * step\n const ticks: RulerTick[] = []\n for (let us = first; us <= endUs + step; us += minorStep) {\n if (us < 0) continue\n ticks.push({\n us: us as Us,\n px: usToPx(us, pxPerSecond),\n major: us % step === 0,\n })\n }\n return ticks\n}\n\n/** 保证值落在 [min, max] */\nexport function clamp(value: number, min: number, max: number): number {\n return Math.min(max, Math.max(min, value))\n}\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"time-CnMfSVYX.cjs","names":[],"sources":["../../src/constants.ts","../../src/lib/id.ts","../../src/timeline/time.ts"],"sourcesContent":["/**\n * cut-core 自有常量。\n *\n * 只放「逻辑层必须知道」的数值:默认时长、默认音量、工程 schema 版本、\n * 历史栈深度。UI 相关的量(缩放档位、面板高度、吸附像素阈值)留在宿主侧,\n * 避免组件库被时间轴界面的调参牵着走。\n */\n\n/** 图片 / 文字素材默认时长(微秒) */\nexport const DEFAULT_STILL_DURATION_US = 5_000_000\n\n/** 默认音量,避免多轨直接相加削波 */\nexport const DEFAULT_CLIP_VOLUME = 0.8\n\n/** 工程 schema 版本 */\nexport const PROJECT_SCHEMA_VERSION = 1\n\n/** 撤销栈最大深度 */\nexport const MAX_HISTORY_STEPS = 50\n\n/**\n * 连续同质操作的合并时间窗口(毫秒)。\n *\n * 拖动滑块 / 画布内拖拽素材时会连续触发大量更新;窗口内相同 mergeKey 的记录\n * 会被合并成一步撤销。取值兼顾「连续拖动算一次」与「停顿后再改算新一步」。\n */\nexport const HISTORY_MERGE_WINDOW_MS = 800\n","import { nanoid } from 'nanoid'\n\n/**\n * 生成短 id。scope 仅用于可读性(例如 `clip_xxx`),不影响唯一性。\n */\nexport function createId(scope?: string): string {\n const id = nanoid(10)\n return scope ? `${scope}_${id}` : id\n}\n","/**\n * 时间与像素的单位换算。\n *\n * 内部统一使用微秒(µs)。为避免中途混用毫秒/秒,这里定义 branded type:\n * - `Us` 微秒\n * - `Ms` 毫秒\n * - `Px` 像素\n *\n * 只在函数边界处做 cast,业务代码不允许出现裸 number 参与时间计算。\n */\n\ndeclare const usBrand: unique symbol\ndeclare const msBrand: unique symbol\ndeclare const pxBrand: unique symbol\n\nexport type Us = number & { readonly [usBrand]: true }\nexport type Ms = number & { readonly [msBrand]: true }\nexport type Px = number & { readonly [pxBrand]: true }\n\nexport const US_PER_SECOND = 1_000_000\nexport const US_PER_MS = 1_000\n\n/** 秒 → 微秒 */\nexport function secToUs(sec: number): Us {\n return Math.round(sec * US_PER_SECOND) as Us\n}\n\n/** 微秒 → 秒 */\nexport function usToSec(us: number): number {\n return us / US_PER_SECOND\n}\n\n/** 毫秒 → 微秒 */\nexport function msToUs(ms: number): Us {\n return Math.round(ms * US_PER_MS) as Us\n}\n\n/** 微秒 → 毫秒 */\nexport function usToMs(us: number): Ms {\n return Math.round(us / US_PER_MS) as Ms\n}\n\n/** 数字 → 微秒(用于从 DOM/WebAV 回调拿到的裸数字) */\nexport function asUs(n: number): Us {\n return Math.round(n) as Us\n}\n\n/** 数字 → 像素 */\nexport function asPx(n: number): Px {\n return n as Px\n}\n\n/**\n * 微秒 → 像素。\n * @param us 时间(微秒)\n * @param pxPerSecond 每秒对应多少像素\n */\nexport function usToPx(us: number, pxPerSecond: number): Px {\n return ((us / US_PER_SECOND) * pxPerSecond) as Px\n}\n\n/**\n * 像素 → 微秒。\n * @param px 像素\n * @param pxPerSecond 每秒对应多少像素\n */\nexport function pxToUs(px: number, pxPerSecond: number): Us {\n return Math.round((px / pxPerSecond) * US_PER_SECOND) as Us\n}\n\n/** 微秒 → `HH:MM:SS.mmm` */\nexport function formatTimecode(us: number, showMs = true): string {\n const safe = Math.max(0, us)\n const totalMs = Math.floor(safe / 1000)\n const ms = totalMs % 1000\n const totalSec = Math.floor(totalMs / 1000)\n const sec = totalSec % 60\n const min = Math.floor(totalSec / 60) % 60\n const hour = Math.floor(totalSec / 3600)\n const pad = (n: number, len = 2) => String(n).padStart(len, '0')\n const base = `${pad(hour)}:${pad(min)}:${pad(sec)}`\n return showMs ? `${base}.${pad(ms, 3)}` : base\n}\n\n/** 微秒 → `MM:SS` 或 `HH:MM:SS`,列表展示用 */\nexport function formatDuration(us: number): string {\n if (!Number.isFinite(us)) return '∞'\n const totalSec = Math.max(0, Math.round(us / US_PER_SECOND))\n const sec = totalSec % 60\n const min = Math.floor(totalSec / 60) % 60\n const hour = Math.floor(totalSec / 3600)\n const pad = (n: number) => String(n).padStart(2, '0')\n return hour > 0 ? `${hour}:${pad(min)}:${pad(sec)}` : `${pad(min)}:${pad(sec)}`\n}\n\nexport interface RulerTick {\n us: Us\n px: Px\n major: boolean\n}\n\nconst TICK_STEPS_US = [\n 100_000, 200_000, 500_000, 1_000_000, 2_000_000, 5_000_000, 10_000_000, 15_000_000,\n 30_000_000, 60_000_000, 120_000_000, 300_000_000, 600_000_000,\n]\n\n/**\n * 根据缩放级别生成刻度尺。\n *\n * @param pxPerSecond 每秒像素\n * @param viewportWidthPx 可视宽度(像素)\n * @param scrollLeftPx 横向滚动偏移(像素)\n * @returns 主刻度(带文字)与次刻度\n */\nexport function buildTicks(\n pxPerSecond: number,\n viewportWidthPx: number,\n scrollLeftPx = 0,\n): RulerTick[] {\n const startUs = pxToUs(scrollLeftPx, pxPerSecond)\n const endUs = pxToUs(scrollLeftPx + viewportWidthPx, pxPerSecond)\n const minLabelSpacingPx = 72\n const step =\n TICK_STEPS_US.find((s) => usToPx(s, pxPerSecond) >= minLabelSpacingPx) ??\n TICK_STEPS_US[TICK_STEPS_US.length - 1]\n const minorStep = step / 5\n const first = Math.floor(startUs / step) * step\n const ticks: RulerTick[] = []\n for (let us = first; us <= endUs + step; us += minorStep) {\n if (us < 0) continue\n ticks.push({\n us: us as Us,\n px: usToPx(us, pxPerSecond),\n major: us % step === 0,\n })\n }\n return ticks\n}\n\n/** 保证值落在 [min, max] */\nexport function clamp(value: number, min: number, max: number): number {\n return Math.min(max, Math.max(min, value))\n}\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"waveform-DJkRtMct.js","names":[],"sources":["../../src/project/schema.ts","../../src/project/indexeddb-storage.ts","../../src/ports/web-bridge.ts","../../src/export/types.ts","../../src/errors.ts","../../src/capability.ts","../../src/editor/event-bus.ts","../../src/editor/history-store.ts","../../../../node_modules/zustand/esm/middleware/immer.mjs","../../src/editor/editor-store.ts","../../src/media/waveform.ts"],"sourcesContent":["/**\r\n * 工程序列化 / 反序列化。\r\n *\r\n * 版本号用于将来迁移。当前 schema v1:Project 原样存储,素材元数据单独存。\r\n *\r\n * 注意:素材二进制(File)不会被持久化到 IndexedDB —— 设计上只在会话内可用,\r\n * 刷新后需要重新导入源文件(提示语里明确说明)。此处仅持久化元数据与时间轴,\r\n * 保证「刷新页面后工程结构能恢复」这一验收项,并给出缺失素材的占位。\r\n */\r\n\r\nimport type { MediaAsset, Project } from '../types'\r\nimport { PROJECT_SCHEMA_VERSION } from '../constants'\r\n\r\nexport interface SerializedProject {\r\n schemaVersion: number\r\n savedAt: number\r\n project: Project\r\n /** 素材元数据;binary 缺失时仅能恢复占位卡片 */\r\n assets: MediaAsset[]\r\n}\r\n\r\nexport interface LoadResult {\r\n project: Project\r\n assets: MediaAsset[]\r\n /** 缺少二进制源的素材 id(UI 用占位显示) */\r\n missingAssetIds: string[]\r\n}\r\n\r\nexport function serializeProject(project: Project, assets: MediaAsset[]): SerializedProject {\r\n return {\r\n schemaVersion: PROJECT_SCHEMA_VERSION,\r\n savedAt: Date.now(),\r\n project,\r\n assets: assets.map(stripRuntimeFields),\r\n }\r\n}\r\n\r\n/** 去掉不可持久化 / 无意义的运行时字段 */\r\nfunction stripRuntimeFields(asset: MediaAsset): MediaAsset {\r\n const { thumbnailUrls: _thumbnailUrls, ...rest } = asset\r\n void _thumbnailUrls\r\n return { ...rest }\r\n}\r\n\r\nexport class ProjectSchemaError extends Error {\r\n constructor(message: string) {\r\n super(message)\r\n this.name = 'ProjectSchemaError'\r\n }\r\n}\r\n\r\nexport function deserializeProject(raw: unknown): LoadResult {\r\n if (raw == null || typeof raw !== 'object') {\r\n throw new ProjectSchemaError('工程数据为空或格式错误')\r\n }\r\n const data = raw as Partial<SerializedProject>\r\n if (typeof data.schemaVersion !== 'number') {\r\n throw new ProjectSchemaError('工程缺少 schemaVersion,无法判断版本')\r\n }\r\n if (data.schemaVersion > PROJECT_SCHEMA_VERSION) {\r\n throw new ProjectSchemaError(\r\n `工程版本 ${data.schemaVersion} 高于当前应用支持的 ${PROJECT_SCHEMA_VERSION},请升级应用`,\r\n )\r\n }\r\n // 迁移入口:未来版本在此按版本号逐步升级\r\n const project = migrate(data.schemaVersion, data.project)\r\n if (project == null) throw new ProjectSchemaError('工程主体缺失')\r\n\r\n const assets = Array.isArray(data.assets) ? data.assets : []\r\n const assetIds = new Set(assets.map((a) => a.id))\r\n\r\n // 两类「缺失」:\r\n // 1. 工程引用了 Assets 表里根本不存在的素材(数据损坏);\r\n // 2. 文件型素材(视频/音频/图片)—— 二进制不会被持久化,恢复后必定没有源文件,\r\n // 需要用户重新导入同名文件才能重连。\r\n const fileBacked = new Set(\r\n assets.filter((a) => a.kind === 'video' || a.kind === 'audio' || a.kind === 'image').map((a) => a.id),\r\n )\r\n const referenced = new Set(project.clips.map((c) => c.assetId))\r\n const missingAssetIds = Array.from(\r\n new Set([\r\n ...project.clips.map((c) => c.assetId).filter((id) => !assetIds.has(id)),\r\n ...Array.from(fileBacked).filter((id) => referenced.has(id)),\r\n ]),\r\n )\r\n return { project, assets, missingAssetIds }\r\n}\r\n\r\nfunction migrate(version: number, project: Project | undefined): Project | null {\r\n if (project == null) return null\r\n switch (version) {\r\n case 1:\r\n default:\r\n return project\r\n }\r\n}\r\n","/**\n * IndexedDB 工程存储(内置默认实现)。\n *\n * 单 object store,key 为命名空间字符串,value 为序列化工程。\n * 一个数据库可以容纳多个工程,靠 namespace 区分(多实例 / 多项目场景)。\n *\n * ⚠️ 浏览器环境之外(Node / SSR)构造时会静默降级为内存存储,\n * 调用方不需要自己做可用性判断。\n */\n\nimport type { SerializedProject } from './schema'\nimport type { StoragePort } from './storage'\n\nexport interface IndexedDbStorageOptions {\n /** 数据库名,默认 `cut-workbench` */\n dbName?: string\n /** 数据库版本,结构变更时递增 */\n version?: number\n}\n\nconst STORE_PROJECTS = 'projects'\n\nfunction isIndexedDbAvailable(): boolean {\n try {\n return typeof indexedDB !== 'undefined' && indexedDB != null\n } catch {\n return false\n }\n}\n\nexport function createIndexedDbStorage(options: IndexedDbStorageOptions = {}): StoragePort {\n const dbName = options.dbName ?? 'cut-workbench'\n const version = options.version ?? 1\n\n // Node / 非安全上下文:退化为内存实现,接口保持一致\n if (!isIndexedDbAvailable()) {\n return createMemoryStorage()\n }\n\n let dbPromise: Promise<IDBDatabase> | null = null\n\n function openDb(): Promise<IDBDatabase> {\n dbPromise ??= new Promise<IDBDatabase>((resolve, reject) => {\n const request = indexedDB.open(dbName, version)\n request.onupgradeneeded = () => {\n const db = request.result\n if (!db.objectStoreNames.contains(STORE_PROJECTS)) {\n db.createObjectStore(STORE_PROJECTS)\n }\n }\n request.onsuccess = () => resolve(request.result)\n request.onerror = () => {\n dbPromise = null\n reject(request.error ?? new Error('IndexedDB 打开失败'))\n }\n })\n return dbPromise\n }\n\n function withStore<T>(\n mode: IDBTransactionMode,\n fn: (store: IDBObjectStore) => IDBRequest<T>,\n ): Promise<T> {\n return openDb().then(\n (db) =>\n new Promise<T>((resolve, reject) => {\n const tx = db.transaction(STORE_PROJECTS, mode)\n const store = tx.objectStore(STORE_PROJECTS)\n const request = fn(store)\n request.onsuccess = () => resolve(request.result)\n request.onerror = () => reject(request.error ?? new Error('IndexedDB 操作失败'))\n tx.onerror = () => reject(tx.error ?? new Error('IndexedDB 事务失败'))\n }),\n )\n }\n\n return {\n async load(namespace) {\n const result = await withStore<SerializedProject | undefined>('readonly', (store) =>\n store.get(namespace),\n )\n return result ?? null\n },\n\n async save(namespace, data) {\n try {\n await withStore('readwrite', (store) => store.put(data, namespace))\n return { ok: true }\n } catch (err) {\n return { ok: false, error: err instanceof Error ? err.message : String(err) }\n }\n },\n\n async remove(namespace) {\n await withStore('readwrite', (store) => store.delete(namespace))\n },\n\n async list() {\n const keys = await withStore<IDBValidKey[]>('readonly', (store) => store.getAllKeys())\n return keys.map(String)\n },\n }\n}\n\n/**\n * 内存存储。\n *\n * 用于:SSR / Node 环境兜底、单元测试、以及「只要内存态不要持久化」的宿主。\n */\nexport function createMemoryStorage(): StoragePort {\n const map = new Map<string, SerializedProject>()\n return {\n async load(namespace) {\n const value = map.get(namespace)\n // 深拷贝,避免调用方改到存储内部的状态\n return value == null ? null : structuredClone(value)\n },\n async save(namespace, data) {\n map.set(namespace, structuredClone(data))\n return { ok: true }\n },\n async remove(namespace) {\n map.delete(namespace)\n },\n async list() {\n return [...map.keys()]\n },\n }\n}\n","/**\n * 浏览器内置宿主桥(默认实现)。\n *\n * 没有传入自定义 bridge 时使用:\n * - `pickFiles` 创建一个隐藏的 `<input type=file>`,用完即弃;\n * - `saveBlob` 走 `<a download>`;\n * - 不实现 `notify`(见文件末尾注释)。\n *\n * 本文件只在浏览器里被调用,不做 SSR 保护(调用方需保证不提前触发)。\n */\n\nimport type { HostBridge, PickFilesResult } from '../ports/host-bridge'\nimport type { SaveFileResult } from '../ports/host-bridge'\nimport type { SerializedProject } from '../project/schema'\n\nexport interface WebBridgeOptions {\n /** 覆盖下载文件名(例如加前缀) */\n downloadName?: (suggested: string) => string\n}\n\n/** 触发浏览器下载;返回是否成功发起 */\nexport function downloadBlob(blob: Blob, filename: string): void {\n const url = URL.createObjectURL(blob)\n const link = document.createElement('a')\n link.href = url\n link.download = filename\n document.body.appendChild(link)\n link.click()\n link.remove()\n // 立即 revoke 会让部分浏览器来不及下载,延迟释放\n window.setTimeout(() => URL.revokeObjectURL(url), 10_000)\n}\n\nexport function createWebBridge(options: WebBridgeOptions = {}): HostBridge {\n return {\n platform: 'web',\n\n files: {\n async pickFiles(pickOptions = {}): Promise<PickFilesResult> {\n return new Promise<File[]>((resolve) => {\n const input = document.createElement('input')\n input.type = 'file'\n input.multiple = pickOptions.multiple ?? true\n if (pickOptions.accept != null && pickOptions.accept.length > 0) {\n input.accept = pickOptions.accept.join(',')\n }\n input.style.display = 'none'\n document.body.appendChild(input)\n input.addEventListener(\n 'change',\n () => {\n const files = Array.from(input.files ?? [])\n input.remove()\n resolve(files)\n },\n { once: true },\n )\n // 用户取消时 change 不触发;靠 window focus 兜底,避免输入框永久挂载\n window.addEventListener(\n 'focus',\n () => {\n window.setTimeout(() => {\n if (document.body.contains(input)) {\n input.remove()\n resolve([])\n }\n }, 300)\n },\n { once: true },\n )\n input.click()\n })\n },\n\n async saveBlob(blob, suggestedName): Promise<SaveFileResult> {\n const name = options.downloadName?.(suggestedName) ?? suggestedName\n downloadBlob(blob, name)\n return { ok: true }\n },\n\n async openProject(): Promise<SerializedProject | null> {\n const files = await this.pickFiles!({ accept: ['.json'], multiple: false })\n // 内置实现(同文件里的 pickFiles)永不返回 null,但类型上是三态;\n // 用 `?? []` 兼容自定义实现返回 null 的情况(视为用户取消)。\n const file = (files ?? [])[0]\n if (file == null) return null\n const text = await file.text()\n return JSON.parse(text) as SerializedProject\n },\n },\n\n // ⚠️ 刻意**不**实现 `notify`。\n //\n // `notify` 存在与否是「宿主是否接管提示」的唯一信号:UI 层据此决定\n // 走宿主还是弹库内 toast。内置桥如果也提供一个「打 console 的 notify」,\n // 这个信号就永远为真 —— 结果是独立使用时提示全被打进 console,\n // 库内 toast 再也不出现(用户什么也看不到)。\n //\n // console 兜底放在 `@cut-workbench/workbench` 的 lib/notify.ts 里,\n // 那里能区分「宿主接管 / 没有宿主」两种情况。\n log(level, message, meta) {\n console[level](`[cut-workbench] ${message}`, meta ?? '')\n },\n }\n}\n","/**\n * 导出能力的公开契约(纯类型 + 纯函数,不依赖 WebAV / React)。\n *\n * 放在根入口可达的位置,宿主可以只依赖 `@cut-workbench/core` 就能标注\n * 导出请求与结果,不必为了类型去加载引擎子入口。\n */\n\nimport type { ExportSettings, Project } from '../types'\n\nexport interface ExportRequest {\n /** 输出参数;缺省为 1080p / 30fps / 8Mbps */\n settings?: Partial<ExportSettings>\n /** 输出 blob 的 MIME,默认 `video/mp4` */\n mimeType?: string\n /** 输出文件名(不含路径),默认按时间生成 */\n filename?: string\n}\n\nexport interface ExportResult {\n blob: Blob\n filename: string\n mimeType: string\n /** 成片时长(微秒)。宿主入库需要 */\n durationUs: number\n width: number\n height: number\n fps: number\n bitrate: number\n elapsedMs: number\n /** 实际执行路径:Worker 或主线程回退(诊断用,宿主可忽略) */\n mode: 'worker' | 'main'\n /** 被跳过的片段(缺源文件 / 类型不支持) */\n skipped: Array<{ clipId: string; reason: string }>\n}\n\nexport interface ExportHandle {\n promise: Promise<ExportResult>\n cancel(): void\n}\n\n/** 导出默认值:1080p / 30fps / 8Mbps / 含音轨 */\nexport const DEFAULT_EXPORT_SETTINGS: ExportSettings = {\n preset: '1080p',\n width: 1920,\n height: 1080,\n fps: 30,\n bitrate: 8_000_000,\n includeAudio: true,\n}\n\nfunction even(n: number): number {\n return Math.max(2, Math.round(n / 2) * 2)\n}\n\n/**\n * 把宿主传入的局部设置补全为完整设置。\n *\n * 只给了 `preset` 时按预设推导尺寸(`source` 取工程尺寸并向下对齐到偶数),\n * 显式给了 `width` / `height` 则以显式值为准。\n */\nexport function resolveExportSettings(\n project: Project,\n partial?: Partial<ExportSettings>,\n): ExportSettings {\n const preset = partial?.preset ?? DEFAULT_EXPORT_SETTINGS.preset\n const presetSize =\n preset === '720p'\n ? { width: 1280, height: 720 }\n : preset === '1080p'\n ? { width: 1920, height: 1080 }\n : { width: even(project.width), height: even(project.height) }\n\n return {\n preset,\n width: partial?.width ?? presetSize.width,\n height: partial?.height ?? presetSize.height,\n fps: partial?.fps ?? DEFAULT_EXPORT_SETTINGS.fps,\n bitrate: partial?.bitrate ?? DEFAULT_EXPORT_SETTINGS.bitrate,\n includeAudio: partial?.includeAudio ?? DEFAULT_EXPORT_SETTINGS.includeAudio,\n }\n}\n\n/** 默认文件名(与编辑器无关,宿主可覆盖) */\nexport function createExportFilename(date = new Date()): string {\n const pad = (n: number) => String(n).padStart(2, '0')\n return `cut-test-${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}-${pad(\n date.getHours(),\n )}${pad(date.getMinutes())}${pad(date.getSeconds())}.mp4`\n}\n","/**\n * 对外错误类型。\n *\n * 放在根入口(不依赖 WebAV / React),宿主可以 `instanceof` 判断后决定\n * 是否弹错误框 —— 用户主动取消导出不是错误,不该被当成失败提示。\n */\n\n/** 导出被 `cancel()` 取消。 */\nexport class ExportCancelledError extends Error {\n constructor(message = '导出已取消') {\n super(message)\n this.name = 'ExportCancelledError'\n }\n}\n\n/** 判断任意异常是否是「用户取消导出」。 */\nexport function isExportCancelled(error: unknown): error is ExportCancelledError {\n return error instanceof ExportCancelledError\n}\n","/**\n * 运行环境能力探测。\n *\n * ⚠️ WebAV 的 `opfs-tools` 在模块加载期就访问 `navigator.storage.getDirectory()`,\n * 因此任何 `import '@webav/*'` 之前都必须先探测,否则在无 OPFS 的环境会直接白屏。\n * 本文件刻意 **不** 静态 import WebAV,调用方应当先 `detectEnvironment()`。\n */\n\n/** 缺失引擎依赖时给出的安装命令(阻断页与 console 共用同一份文案) */\nexport const ENGINE_DEPENDENCIES_INSTALL_COMMAND =\n 'npm i @webav/av-cliper @webav/av-canvas @webav/av-recorder'\n\nexport interface CapabilityReport {\n /** WebCodecs 四个核心接口是否齐全 */\n webCodecs: boolean\n /** H.264 编码是否受支持(Combinator.isSupported 的简化替代) */\n videoEncode: boolean\n /** AAC 音频编码是否受支持 */\n audioEncode: boolean\n /** OffscreenCanvas 可用 */\n offscreenCanvas: boolean\n /** OPFS 是否可用(WebAV 硬依赖) */\n opfs: boolean\n /** WebGL2 是否存在(createChromakey 需要) */\n webgl2: boolean\n /** WebAV 提供者 Combinator.isSupported 的最终结论 */\n composerSupported: boolean\n /**\n * 引擎依赖能否解析到真实模块。\n *\n * 依赖缺失属于「装包问题」而不是「浏览器不支持」,因此在报告里单列 ——\n * 阻断页要给出可执行的安装命令,而不是让人去换浏览器。\n */\n dependencies: {\n '@webav/av-cliper': boolean\n '@webav/av-canvas': boolean\n }\n /** 缺失的 npm 包名;空数组表示齐全。阻断页据此渲染安装命令 */\n missingDependencies: string[]\n /** 逐项人类可读说明,失败项会出现在阻断页上 */\n issues: string[]\n /** 建议 */\n advice: string\n}\n\nfunction hasWebCodecs(): boolean {\n return (\n typeof globalThis.VideoEncoder !== 'undefined' &&\n typeof globalThis.VideoDecoder !== 'undefined' &&\n typeof globalThis.AudioEncoder !== 'undefined' &&\n typeof globalThis.AudioDecoder !== 'undefined' &&\n typeof globalThis.VideoFrame !== 'undefined' &&\n typeof globalThis.AudioData !== 'undefined'\n )\n}\n\nfunction hasWebGL2(): boolean {\n try {\n const canvas = document.createElement('canvas')\n return canvas.getContext('webgl2') != null\n } catch {\n return false\n }\n}\n\nexport async function detectOpfs(): Promise<boolean> {\n try {\n if (typeof navigator === 'undefined' || navigator.storage?.getDirectory == null) {\n return false\n }\n await navigator.storage.getDirectory()\n return true\n } catch {\n return false\n }\n}\n\n/**\n * 探测引擎依赖是否真的可解析。\n *\n * 三种失败形态都要覆盖,且都必须**不抛错**:\n * 1. 运行时 `Cannot find module` —— 动态 import 直接 reject;\n * 2. 构建期缺失(npm 的 optional peer 桩模块)—— import 成功但模块是空的,\n * 期望的导出一律 `undefined`;\n * 3. 版本不匹配 —— 导出存在但不是预期的形状。\n *\n * 因此判定依据是「关键导出存在」而不是「import 成功」。\n */\nasync function probeDependency(\n load: () => Promise<Record<string, unknown>>,\n requiredExports: string[],\n): Promise<boolean> {\n try {\n const mod = await load()\n // 桩模块在 Vite 里是 `export default {}` 形态,具名导出全是 undefined\n const target = (mod as { default?: Record<string, unknown> }).default ?? mod\n return requiredExports.every((key) => (target as Record<string, unknown>)[key] != null)\n } catch {\n return false\n }\n}\n\nexport async function detectDependencies(): Promise<CapabilityReport['dependencies']> {\n const [cliper, canvas] = await Promise.all([\n probeDependency(() => import('@webav/av-cliper'), ['Combinator']),\n probeDependency(() => import('@webav/av-canvas'), ['AVCanvas']),\n ])\n return { '@webav/av-cliper': cliper, '@webav/av-canvas': canvas }\n}\n\n/**\n * 探测当前浏览器是否满足 WebAV 运行条件。\n *\n * 顺序是有意为之:先查依赖是否装上,再查浏览器是否支持。\n * 依赖缺失时**不再**尝试 import WebAV —— 桩模块的 `Combinator.isSupported()`\n * 只会抛一个难以理解的 TypeError,掩盖真正的原因(没装包)。\n */\nexport async function detectEnvironment(): Promise<CapabilityReport> {\n const issues: string[] = []\n const webCodecs = hasWebCodecs()\n const offscreenCanvas = typeof OffscreenCanvas !== 'undefined'\n const webgl2 = hasWebGL2()\n\n const dependencies = await detectDependencies()\n const missingDependencies = Object.entries(dependencies)\n .filter(([, ok]) => !ok)\n .map(([name]) => name)\n\n if (missingDependencies.length > 0) {\n issues.push(\n `缺少引擎依赖:${missingDependencies.join('、')}。` +\n `请执行 \\`${ENGINE_DEPENDENCIES_INSTALL_COMMAND}\\` 后重新构建。`,\n )\n }\n\n if (!webCodecs) {\n issues.push('当前浏览器缺少 WebCodecs(VideoEncoder / VideoDecoder / AudioEncoder)。')\n }\n if (!offscreenCanvas) {\n issues.push('当前浏览器不支持 OffscreenCanvas。')\n }\n\n const opfs = await detectOpfs()\n if (!opfs) {\n issues.push(\n 'OPFS 不可用(navigator.storage.getDirectory 访问失败)。WebAV 依赖它读写临时文件,' +\n '常见原因是隐私模式或站点数据被完全禁用。',\n )\n }\n\n let videoEncode = false\n let audioEncode = false\n let composerSupported = false\n\n if (missingDependencies.length === 0 && webCodecs && opfs) {\n try {\n const [{ Combinator }, videoSupport, audioSupport] = await Promise.all([\n import('@webav/av-cliper'),\n VideoEncoder.isConfigSupported({\n codec: 'avc1.42E032',\n width: 1920,\n height: 1080,\n bitrate: 5_000_000,\n }),\n AudioEncoder.isConfigSupported({\n codec: 'mp4a.40.2',\n sampleRate: 48000,\n numberOfChannels: 2,\n }),\n ])\n videoEncode = videoSupport.supported ?? false\n audioEncode = audioSupport.supported ?? false\n composerSupported = await Combinator.isSupported()\n if (!videoEncode) issues.push('当前设备无法编码 H.264(avc1.42E032)。')\n if (!audioEncode) issues.push('当前设备无法编码 AAC(mp4a.40.2)。')\n if (!composerSupported) issues.push('WebAV Combinator 自检未通过。')\n } catch (err) {\n issues.push(`WebAV 初始化失败:${err instanceof Error ? err.message : String(err)}`)\n }\n }\n\n // 依赖缺失和浏览器不支持是两类问题,建议也必须分开给:\n // 让人对着「请换浏览器」去修一个装包问题是最糟的体验。\n const advice =\n missingDependencies.length > 0\n ? `请先安装引擎依赖:${ENGINE_DEPENDENCIES_INSTALL_COMMAND}`\n : issues.length === 0\n ? '环境满足全部要求,可正常使用。'\n : '建议使用最新版 Chrome 或 Edge(Chromium 内核),并关闭无痕模式后重试。'\n\n return {\n webCodecs,\n videoEncode,\n audioEncode,\n offscreenCanvas,\n opfs,\n webgl2,\n composerSupported,\n dependencies,\n missingDependencies,\n issues,\n advice,\n }\n}\n\n/** 只要满足最低可用条件即允许进入编辑器 */\nexport function isEnvironmentUsable(report: CapabilityReport | null): boolean {\n if (!report) return false\n if (report.missingDependencies.length > 0) return false\n return report.opfs && report.composerSupported\n}\n\n/** 文本摘要,用于 TopBar 徽标 */\nexport function summarizeCapabilities(report: CapabilityReport | null): string {\n if (!report) return '探测中'\n if (report.missingDependencies.length > 0) return '缺少引擎依赖'\n if (report.issues.length === 0) return '环境就绪'\n return `${report.issues.length} 项异常`\n}\n","/**\r\n * 类型化事件总线。\r\n *\r\n * 为什么不用 mitt / eventemitter3:\r\n * - 需要 `emit` 时隔离监听器异常(一个订阅方抛错不能打断其他订阅方);\r\n * - 需要 `on()` 返回 unsubscribe(React effect 里直接用);\r\n * - 需要对宿主侧做类型收窄(宿主只能订阅,不能 emit)。\r\n *\r\n * 总线是**每个 Editor 实例一份**,不做模块级单例,否则多实例会互相串台。\r\n */\r\n\r\n// 用接口约束而不是 Record<string, unknown>:后者的索引签名会拒绝普通 interface\r\nexport type EventMap = object\r\n\r\nexport type Unsubscribe = () => void\r\n\r\ntype Handler<Payload> = (payload: Payload) => void\r\n\r\nexport interface EventBus<M extends EventMap> {\r\n /** 订阅;相同 handler 重复订阅会被忽略 */\r\n on<K extends keyof M & string>(type: K, handler: Handler<M[K]>): Unsubscribe\r\n /** 订阅一次 */\r\n once<K extends keyof M & string>(type: K, handler: Handler<M[K]>): Unsubscribe\r\n /** 退订;不传 handler 时清空该类型 */\r\n off<K extends keyof M & string>(type: K, handler?: Handler<M[K]>): void\r\n /** 派发;单个监听器抛错不影响其他监听器 */\r\n emit<K extends keyof M & string>(type: K, payload: M[K]): void\r\n /** 清空所有订阅(dispose 时调用) */\r\n clear(): void\r\n /** 当前某类型的订阅数(调试用) */\r\n listenerCount<K extends keyof M & string>(type: K): number\r\n}\r\n\r\nexport function createEventBus<M extends EventMap>(onError?: (error: unknown) => void): EventBus<M> {\r\n const listeners = new Map<string, Set<Handler<never>>>()\r\n\r\n function ensure(type: string): Set<Handler<never>> {\r\n let set = listeners.get(type)\r\n if (set == null) {\r\n set = new Set()\r\n listeners.set(type, set)\r\n }\r\n return set\r\n }\r\n\r\n return {\r\n on(type, handler) {\r\n const set = ensure(type)\r\n set.add(handler as Handler<never>)\r\n return () => {\r\n set.delete(handler as Handler<never>)\r\n }\r\n },\r\n\r\n once(type, handler) {\r\n const unsubscribe = this.on(type, (payload) => {\r\n unsubscribe()\r\n handler(payload)\r\n })\r\n return unsubscribe\r\n },\r\n\r\n off(type, handler) {\r\n if (handler == null) {\r\n listeners.delete(type)\r\n return\r\n }\r\n listeners.get(type)?.delete(handler as Handler<never>)\r\n },\r\n\r\n emit(type, payload) {\r\n const set = listeners.get(type)\r\n if (set == null || set.size === 0) return\r\n // 复制一份再遍历:监听器内部可能退订自己\r\n for (const handler of [...set]) {\r\n try {\r\n ;(handler as Handler<unknown>)(payload)\r\n } catch (err) {\r\n onError?.(err)\r\n }\r\n }\r\n },\r\n\r\n clear() {\r\n listeners.clear()\r\n },\r\n\r\n listenerCount(type) {\r\n return listeners.get(type)?.size ?? 0\r\n },\r\n }\r\n}\r\n\r\n/** 宿主持有的事件视图:只能订阅,不能派发 */\r\nexport type EventEmitterView<M extends EventMap> = Pick<EventBus<M>, 'on' | 'once' | 'off'>\r\n","/**\n * 撤销/重做历史栈(最多 50 步快照)。\n *\n * 与编辑器 store 解耦:这里只负责保存快照,编辑器在变更**前**调用 `record()`,\n * undo/redo 时取出快照整体替换。\n *\n * 为什么整体快照而不是命令式 patch:时间轴操作(分割/裁剪/吸附)彼此耦合,\n * 整体快照在 200 clip 量级下也只有几百 KB,换来的是绝对正确的撤销语义。\n *\n * 合并(coalescing):连续的高频同质操作(拖动滑块、画布内拖动素材)若每步都压栈,\n * 一次拖动就会产生几十条历史,用户按 Ctrl+Z 时感觉「撤销不动」。\n * 因此 `record()` 支持 `mergeKey`:同一 key 在时间窗口内的后续记录会被丢弃,\n * 栈里保留的始终是「本次操作开始前」的快照 —— 一次拖动 = 一步撤销。\n *\n * 使用 zustand 的 **vanilla** 版本创建:不绑定 React,Core 依旧零 React 依赖。\n */\n\nimport { createStore, type StoreApi } from 'zustand/vanilla'\nimport type { MediaAsset, Project } from '../types'\nimport { HISTORY_MERGE_WINDOW_MS, MAX_HISTORY_STEPS } from '../constants'\n\nexport interface EditorSnapshot {\n project: Project\n assets: MediaAsset[]\n /** 该快照产生的原因,便于调试与 UI 提示 */\n label?: string\n}\n\nexport interface RecordOptions {\n label?: string\n /**\n * 合并键。相同 key 在 `HISTORY_MERGE_WINDOW_MS` 内的连续记录会被合并为一步。\n *\n * 约定格式:`${操作类型}:${目标 id}`,例如 `slider:clip_xx:opacity`。\n * 不传则每次都是独立一步。\n */\n mergeKey?: string\n}\n\nexport interface HistoryState {\n past: EditorSnapshot[]\n future: EditorSnapshot[]\n /** 上一条记录的合并键(用于同一会话内的连续合并) */\n lastMergeKey: string | null\n /** 上一条记录的写入时间 */\n lastRecordedAt: number\n record: (snapshot: EditorSnapshot, options?: RecordOptions) => void\n undo: (current: EditorSnapshot) => EditorSnapshot | null\n redo: (current: EditorSnapshot) => EditorSnapshot | null\n clear: () => void\n /** 结束当前合并会话:之后即使 mergeKey 相同也会新起一步 */\n endMerge: () => void\n canUndo: () => boolean\n canRedo: () => boolean\n}\n\nexport type HistoryStore = StoreApi<HistoryState>\n\nfunction cloneSnapshot(snapshot: EditorSnapshot): EditorSnapshot {\n return {\n project: structuredClone(snapshot.project),\n assets: structuredClone(snapshot.assets),\n label: snapshot.label,\n }\n}\n\n/** 每个 Editor 实例一份,不复用 */\nexport function createHistoryStore(): HistoryStore {\n return createStore<HistoryState>()((set, get) => ({\n past: [],\n future: [],\n lastMergeKey: null,\n lastRecordedAt: 0,\n\n record: (snapshot, options) => {\n const state = get()\n const mergeKey = options?.mergeKey ?? null\n const now = Date.now()\n\n // 连续同质操作:保留「第一次操作前」的快照,丢弃后续的,从而实现一步撤销\n if (\n mergeKey != null &&\n mergeKey === state.lastMergeKey &&\n now - state.lastRecordedAt < HISTORY_MERGE_WINDOW_MS\n ) {\n set({ lastRecordedAt: now, future: [] })\n return\n }\n\n const labeled: EditorSnapshot = options?.label\n ? { ...snapshot, label: options.label }\n : snapshot\n\n set((current) => {\n const past = [...current.past, cloneSnapshot(labeled)]\n return {\n past: past.length > MAX_HISTORY_STEPS ? past.slice(past.length - MAX_HISTORY_STEPS) : past,\n future: [],\n lastMergeKey: mergeKey,\n lastRecordedAt: now,\n }\n })\n },\n\n undo: (current) => {\n const { past } = get()\n const previous = past[past.length - 1]\n if (!previous) return null\n set((state) => ({\n past: state.past.slice(0, -1),\n future: [...state.future, cloneSnapshot(current)],\n // 撤销后必须断开合并:否则紧接着的同类编辑会错误地并进上一步\n lastMergeKey: null,\n lastRecordedAt: 0,\n }))\n return previous\n },\n\n redo: (current) => {\n const { future } = get()\n const next = future[future.length - 1]\n if (!next) return null\n set((state) => ({\n future: state.future.slice(0, -1),\n past: [...state.past, cloneSnapshot(current)],\n lastMergeKey: null,\n lastRecordedAt: 0,\n }))\n return next\n },\n\n clear: () => set({ past: [], future: [], lastMergeKey: null, lastRecordedAt: 0 }),\n\n endMerge: () => set({ lastMergeKey: null, lastRecordedAt: 0 }),\n\n canUndo: () => get().past.length > 0,\n canRedo: () => get().future.length > 0,\n }))\n}\n","import { produce } from 'immer';\n\nconst immerImpl = (initializer) => (set, get, store) => {\n store.setState = (updater, replace, ...args) => {\n const nextState = typeof updater === \"function\" ? produce(updater) : updater;\n return set(nextState, replace, ...args);\n };\n return initializer(store.setState, get, store);\n};\nconst immer = immerImpl;\n\nexport { immer };\n","/**\n * 编辑器主状态(zustand vanilla + immer)。\n *\n * 分三层:store 依赖 core,core 不依赖 React;UI 侧通过 `zustand/react` 或\n * `useSyncExternalStore` 绑定。\n *\n * 时间单位统一微秒;`selection` / `playheadUs` 属于 UI 状态但和编辑强相关,放这里。\n *\n * 历史记录由独立的 history store 统一管理:每次「结构性变更」前把当前快照压栈。\n * 高频交互(拖拽过程)传 `recordHistory = false`,只在松手时记一次。\n *\n * ⚠️ 使用工厂而非模块级单例:同一页面挂载两个工作台、或 Electron 多窗口时,\n * 每个实例必须拥有独立的 store,否则会共享工程数据。\n */\n\nimport { createStore, type StoreApi } from 'zustand/vanilla'\nimport { immer } from 'zustand/middleware/immer'\nimport type { MediaAsset, Project, TimelineClip, Track } from '../types'\nimport { createEmptyProject, createTrack, getClipEnd, getProjectDuration } from '../timeline/model'\nimport {\n compactTrackData,\n duplicateClipsData,\n removeClipsData,\n trimClipData,\n} from '../timeline/ops'\nimport { asUs } from '../timeline/time'\nimport { normalizeSize, remapClipsForSize } from '../timeline/canvas-size'\nimport { createId } from '../lib/id'\nimport type { HistoryStore } from './history-store'\n\nexport interface EditorState {\n project: Project\n assets: MediaAsset[]\n selectedClipIds: string[]\n selectedAssetId: string | null\n playheadUs: number\n playing: boolean\n masterVolume: number\n dirty: boolean\n /**\n * 缺少源文件的素材 id。\n *\n * 素材二进制不会被持久化(File 无法存进 IndexedDB),因此刷新后恢复的工程里\n * clip 仍存在但素材已不可用。UI 据此渲染「缺失素材」占位,并静默跳过预览,\n * 而不是每个 clip 都弹一次错误。\n */\n missingAssetIds: string[]\n\n setProject: (project: Project, assets: MediaAsset[], missingAssetIds?: string[]) => void\n resetProject: () => void\n renameProject: (name: string) => void\n setProjectSize: (width: number, height: number, mode?: 'scale' | 'keep') => void\n /** 运行期改工程 id(宿主把临时工程归入某项目时用) */\n setProjectId: (id: string) => void\n /** 整体替换「缺失素材」列表(importJson 补齐二进制后回写) */\n setMissingAssetIds: (ids: string[]) => void\n\n addAsset: (asset: MediaAsset) => void\n clearMissing: (assetId: string) => void\n replaceAsset: (assetId: string, next: MediaAsset) => void\n updateAsset: (assetId: string, patch: Partial<MediaAsset>) => void\n removeAsset: (assetId: string) => void\n\n addClip: (clip: TimelineClip) => void\n addClips: (clips: TimelineClip[]) => void\n /**\n * 修改片段属性。\n *\n * @param recordHistoryFlag 是否记录历史(拖拽过程可传 false)\n * @param mergeKey 合并键;拖动滑块这类高频连续修改必须传,\n * 否则一次拖动会产生几十条撤销记录。\n */\n updateClip: (\n clipId: string,\n patch: Partial<TimelineClip>,\n recordHistoryFlag?: boolean,\n mergeKey?: string,\n ) => void\n updateClips: (patches: Array<{ id: string; patch: Partial<TimelineClip> }>) => void\n removeClips: (ids: string[]) => void\n moveClip: (clipId: string, trackId: string, startUs: number, recordHistory?: boolean) => void\n trimClip: (clipId: string, edge: 'start' | 'end', deltaUs: number, recordHistory?: boolean) => void\n splitClipsAt: (atUs: number, clipIds?: string[]) => string[]\n duplicateSelection: (offsetUs: number) => Array<{ sourceId: string; newId: string }>\n compactTrack: (trackId: string) => void\n\n addTrack: (kind: Track['kind']) => string\n removeTrack: (trackId: string) => void\n updateTrack: (trackId: string, patch: Partial<Track>) => void\n\n selectClips: (ids: string[]) => void\n toggleSelectClip: (id: string) => void\n selectAsset: (id: string | null) => void\n setPlayhead: (us: number) => void\n setPlaying: (playing: boolean) => void\n setMasterVolume: (v: number) => void\n\n undo: () => void\n redo: () => void\n markSaved: () => void\n}\n\nexport type EditorStore = StoreApi<EditorState>\n\nexport interface CreateEditorStoreOptions {\n history: HistoryStore\n project?: Project\n assets?: MediaAsset[]\n}\n\n/**\n * 创建编辑器 store。\n *\n * @param options.history 与之配对的历史栈(每个编辑器一个,不要共享)\n */\nexport function createEditorStore(options: CreateEditorStoreOptions): EditorStore {\n const history = options.history\n const store = createStore<EditorState>()(\n immer((set, get) => {\n /** 在结构性变更前压栈(必须在 set 之前调用,拿到的是「变更前」的完整快照) */\n function recordHistory(label: string, mergeKey?: string): void {\n const { project, assets } = get()\n history.getState().record(\n {\n project: structuredClone(project),\n assets: structuredClone(assets),\n label,\n },\n mergeKey != null ? { label, mergeKey } : { label },\n )\n }\n\n return {\n project: options.project ?? createEmptyProject(),\n assets: options.assets ?? [],\n selectedClipIds: [],\n selectedAssetId: null,\n playheadUs: 0,\n playing: false,\n masterVolume: 1,\n dirty: false,\n missingAssetIds: [],\n\n setProject: (project, assets, missingAssetIds = []) =>\n set((draft) => {\n draft.project = project\n draft.assets = assets\n draft.missingAssetIds = missingAssetIds\n draft.selectedClipIds = []\n draft.selectedAssetId = null\n draft.playheadUs = 0\n draft.dirty = false\n }),\n\n resetProject: () =>\n set((draft) => {\n draft.project = createEmptyProject()\n draft.assets = []\n draft.missingAssetIds = []\n draft.selectedClipIds = []\n draft.selectedAssetId = null\n draft.playheadUs = 0\n draft.playing = false\n draft.dirty = false\n }),\n\n renameProject: (name) => {\n recordHistory('重命名工程')\n set((draft) => {\n draft.project.name = name\n draft.project.updatedAt = Date.now()\n draft.dirty = true\n })\n },\n\n setProjectId: (id) => {\n if (id === get().project.id) return\n recordHistory('修改工程 ID')\n set((draft) => {\n draft.project.id = id\n draft.project.updatedAt = Date.now()\n draft.dirty = true\n })\n },\n\n setMissingAssetIds: (ids) =>\n set((draft) => {\n draft.missingAssetIds = [...ids]\n }),\n\n /**\n * 修改画布尺寸(比例)。\n *\n * 素材的 `transform` 以工程分辨率为坐标系,因此换尺寸时必须同步重映射,\n * 否则「铺满画面」的素材在新画布里会偏到一角甚至完全跑到画外。\n *\n * @param mode `scale`(默认)素材随画布等比缩放保持构图;`keep` 素材坐标不变\n */\n setProjectSize: (width, height, mode = 'scale') => {\n const current = get().project\n const next = normalizeSize(width, height)\n if (next.width === current.width && next.height === current.height) return\n recordHistory('修改画布尺寸')\n set((draft) => {\n const from = { width: draft.project.width, height: draft.project.height }\n draft.project.width = next.width\n draft.project.height = next.height\n draft.project.clips = remapClipsForSize(\n draft.project.clips,\n from,\n next,\n mode,\n ) as typeof draft.project.clips\n draft.project.updatedAt = Date.now()\n draft.dirty = true\n })\n },\n\n addAsset: (asset) =>\n set((draft) => {\n draft.assets.push(asset)\n draft.dirty = true\n }),\n\n /** 重新导入素材后,解除「缺失」标记(按 id 匹配) */\n clearMissing: (assetId) =>\n set((draft) => {\n draft.missingAssetIds = draft.missingAssetIds.filter((id) => id !== assetId)\n }),\n\n /**\n * 用新导入的素材替换旧记录(保留原 id)。\n *\n * 用于「刷新后重新导入同名文件」的重连:assetId 不变,时间轴上引用它的 clip\n * 无需改动即可恢复。二进制替换由 ClipRegistry.replaceAssetSource 负责。\n */\n replaceAsset: (assetId, next) =>\n set((draft) => {\n const index = draft.assets.findIndex((a) => a.id === assetId)\n if (index < 0) return\n draft.assets[index] = next\n draft.project.updatedAt = Date.now()\n draft.dirty = true\n }),\n\n updateAsset: (assetId, patch) =>\n set((draft) => {\n const asset = draft.assets.find((a) => a.id === assetId)\n if (asset) Object.assign(asset, patch)\n draft.dirty = true\n }),\n\n removeAsset: (assetId) => {\n recordHistory('删除素材')\n set((draft) => {\n draft.assets = draft.assets.filter((a) => a.id !== assetId)\n const removed = new Set(\n draft.project.clips.filter((c) => c.assetId === assetId).map((c) => c.id),\n )\n draft.project.clips = draft.project.clips.filter((c) => !removed.has(c.id))\n draft.selectedClipIds = draft.selectedClipIds.filter((id) => !removed.has(id))\n if (draft.selectedAssetId === assetId) draft.selectedAssetId = null\n draft.project.updatedAt = Date.now()\n draft.dirty = true\n })\n },\n\n addClip: (clip) => {\n recordHistory('添加片段')\n set((draft) => {\n draft.project.clips.push(clip)\n draft.selectedClipIds = [clip.id]\n draft.project.updatedAt = Date.now()\n draft.dirty = true\n })\n },\n\n addClips: (clips) => {\n if (clips.length === 0) return\n recordHistory('添加片段')\n set((draft) => {\n draft.project.clips.push(...clips)\n draft.selectedClipIds = clips.map((c) => c.id)\n draft.project.updatedAt = Date.now()\n draft.dirty = true\n })\n },\n\n updateClip: (clipId, patch, recordHistoryFlag = true, mergeKey) => {\n if (recordHistoryFlag) recordHistory('修改片段属性', mergeKey)\n set((draft) => {\n const clip = draft.project.clips.find((c) => c.id === clipId)\n if (clip) Object.assign(clip, patch)\n draft.project.updatedAt = Date.now()\n draft.dirty = true\n })\n },\n\n updateClips: (patches) => {\n recordHistory('批量修改片段')\n set((draft) => {\n for (const { id, patch } of patches) {\n const clip = draft.project.clips.find((c) => c.id === id)\n if (clip) Object.assign(clip, patch)\n }\n draft.project.updatedAt = Date.now()\n draft.dirty = true\n })\n },\n\n removeClips: (ids) => {\n if (ids.length === 0) return\n recordHistory('删除片段')\n set((draft) => {\n draft.project.clips = removeClipsData(draft.project.clips, ids)\n draft.selectedClipIds = draft.selectedClipIds.filter((id) => !ids.includes(id))\n draft.project.updatedAt = Date.now()\n draft.dirty = true\n })\n },\n\n moveClip: (clipId, trackId, startUs, recordHistoryFlag = true) => {\n if (recordHistoryFlag) recordHistory('移动片段')\n set((draft) => {\n const clip = draft.project.clips.find((c) => c.id === clipId)\n if (!clip) return\n clip.trackId = trackId\n clip.startUs = asUs(Math.max(0, startUs))\n draft.project.updatedAt = Date.now()\n draft.dirty = true\n })\n },\n\n trimClip: (clipId, edge, deltaUs, recordHistoryFlag = true) => {\n if (recordHistoryFlag) recordHistory('裁剪片段')\n set((draft) => {\n const index = draft.project.clips.findIndex((c) => c.id === clipId)\n const clip = draft.project.clips[index]\n if (!clip) return\n draft.project.clips[index] = trimClipData(clip, edge, deltaUs)\n draft.project.updatedAt = Date.now()\n draft.dirty = true\n })\n },\n\n splitClipsAt: (atUs, clipIds) => {\n const state = get()\n const targets = state.project.clips.filter(\n (c) =>\n (clipIds == null || clipIds.includes(c.id)) &&\n atUs > c.startUs &&\n atUs < getClipEnd(c),\n )\n if (targets.length === 0) return []\n\n // ⚠️ 关键:右半段必须在进入 immer draft 之前算好。\n // draft 里的对象是 Proxy,`structuredClone(proxy)` 会抛 DataCloneError。\n const patches = targets.map((clip) => {\n const end = getClipEnd(clip)\n const leftDuration = atUs - clip.startUs\n const rightDuration = end - atUs\n const sourceOffset = Math.round(leftDuration * clip.playbackRate)\n const right: TimelineClip = {\n ...structuredClone(clip),\n id: createId('clip'),\n startUs: asUs(atUs),\n durationUs: asUs(rightDuration),\n inPointUs: asUs(clip.inPointUs + sourceOffset),\n fadeInUs: asUs(Math.min(clip.fadeInUs, rightDuration)),\n }\n return {\n leftId: clip.id,\n leftDuration: asUs(leftDuration),\n leftFadeOut: asUs(Math.min(clip.fadeOutUs, leftDuration)),\n right,\n }\n })\n\n recordHistory('分割片段')\n const newIds = patches.map((p) => p.right.id)\n set((draft) => {\n for (const patch of patches) {\n const left = draft.project.clips.find((c) => c.id === patch.leftId)\n if (left) {\n left.durationUs = patch.leftDuration\n left.fadeOutUs = patch.leftFadeOut\n }\n draft.project.clips.push(patch.right)\n }\n draft.selectedClipIds = newIds\n draft.project.updatedAt = Date.now()\n draft.dirty = true\n })\n return newIds\n },\n\n duplicateSelection: (offsetUs) => {\n const state = get()\n const ids = state.selectedClipIds\n const created: Array<{ sourceId: string; newId: string }> = []\n if (ids.length === 0) return created\n\n // 同样在 draft 外计算副本(避免 structuredClone proxy)\n const pairs = duplicateClipsData(\n structuredClone(state.project.clips),\n ids,\n offsetUs,\n () => createId('clip'),\n )\n recordHistory('复制片段')\n set((draft) => {\n for (const { sourceId, clip } of pairs) {\n draft.project.clips.push(clip)\n created.push({ sourceId, newId: clip.id })\n }\n draft.selectedClipIds = created.map((c) => c.newId)\n draft.project.updatedAt = Date.now()\n draft.dirty = true\n })\n return created\n },\n\n compactTrack: (trackId) => {\n recordHistory('轨道磁吸排序')\n set((draft) => {\n draft.project.clips = compactTrackData(draft.project.clips, trackId)\n draft.project.updatedAt = Date.now()\n draft.dirty = true\n })\n },\n\n addTrack: (kind) => {\n const track = createTrack(\n kind,\n `${kind === 'visual' ? '视频' : '音频'} ${get().project.tracks.filter((t) => t.kind === kind).length + 1}`,\n )\n recordHistory('添加轨道')\n set((draft) => {\n draft.project.tracks.push(track)\n draft.project.updatedAt = Date.now()\n draft.dirty = true\n })\n return track.id\n },\n\n removeTrack: (trackId) => {\n recordHistory('删除轨道')\n set((draft) => {\n if (draft.project.tracks.length <= 1) return\n draft.project.tracks = draft.project.tracks.filter((t) => t.id !== trackId)\n draft.project.clips = draft.project.clips.filter((c) => c.trackId !== trackId)\n draft.project.updatedAt = Date.now()\n draft.dirty = true\n })\n },\n\n updateTrack: (trackId, patch) =>\n set((draft) => {\n const track = draft.project.tracks.find((t) => t.id === trackId)\n if (track) Object.assign(track, patch)\n draft.dirty = true\n }),\n\n selectClips: (ids) =>\n set((draft) => {\n draft.selectedClipIds = ids\n }),\n\n toggleSelectClip: (id) =>\n set((draft) => {\n const index = draft.selectedClipIds.indexOf(id)\n if (index >= 0) draft.selectedClipIds.splice(index, 1)\n else draft.selectedClipIds.push(id)\n }),\n\n selectAsset: (id) =>\n set((draft) => {\n draft.selectedAssetId = id\n }),\n\n setPlayhead: (us) =>\n set((draft) => {\n draft.playheadUs = asUs(Math.max(0, us))\n }),\n\n setPlaying: (playing) =>\n set((draft) => {\n draft.playing = playing\n }),\n\n setMasterVolume: (v) =>\n set((draft) => {\n draft.masterVolume = Math.max(0, Math.min(1, v))\n }),\n\n undo: () => {\n const { project, assets } = get()\n const restored = history.getState().undo({ project, assets })\n if (!restored) return\n set((draft) => {\n draft.project = restored.project\n draft.assets = restored.assets\n draft.selectedClipIds = draft.selectedClipIds.filter((id) =>\n restored.project.clips.some((c) => c.id === id),\n )\n draft.dirty = true\n })\n },\n\n redo: () => {\n const { project, assets } = get()\n const restored = history.getState().redo({ project, assets })\n if (!restored) return\n set((draft) => {\n draft.project = restored.project\n draft.assets = restored.assets\n draft.dirty = true\n })\n },\n\n markSaved: () =>\n set((draft) => {\n draft.dirty = false\n }),\n }\n }),\n )\n return store\n}\n\n// —— 细粒度 selector(避免一次拖动重渲染整个时间轴) ——\n\nexport const selectProject = (s: EditorState) => s.project\nexport const selectAssets = (s: EditorState) => s.assets\nexport const selectSelectedClipIds = (s: EditorState) => s.selectedClipIds\nexport const selectPlayheadUs = (s: EditorState) => s.playheadUs\nexport const selectProjectDuration = (s: EditorState) => getProjectDuration(s.project)\nexport const selectTracks = (s: EditorState) => s.project.tracks\nexport const selectClips = (s: EditorState) => s.project.clips\n\nexport function selectClipById(id: string) {\n return (s: EditorState) => s.project.clips.find((c) => c.id === id)\n}\n\nexport function selectAssetById(id: string | null) {\n return (s: EditorState) => (id == null ? undefined : s.assets.find((a) => a.id === id))\n}\n","/**\n * 音频波形峰值计算。\n *\n * 输入是 AudioClip 的两条 PCM 声道(Float32Array,48kHz),输出 N 个 0~1 的峰值,\n * 供时间轴 / 素材卡片绘制。\n */\n\n/** 将多声道 PCM 归并为单声道并降采样为峰值数组 */\nexport function computeWaveform(channels: Float32Array[], buckets: number): number[] {\n const left = channels[0]\n if (!left || left.length === 0 || buckets <= 0) return []\n\n const right = channels[1] ?? left\n const perBucket = Math.max(1, Math.floor(left.length / buckets))\n const peaks: number[] = []\n\n for (let b = 0; b < buckets; b += 1) {\n const start = b * perBucket\n const end = Math.min(left.length, start + perBucket)\n let peak = 0\n for (let i = start; i < end; i += 1) {\n const l = Math.abs(left[i] ?? 0)\n const r = Math.abs(right[i] ?? 0)\n const v = l > r ? l : r\n if (v > peak) peak = v\n }\n peaks.push(Math.min(1, peak))\n }\n return peaks\n}\n\n/** 生成波形 SVG path(对称柱状) */\nexport function buildWaveformPath(peaks: number[], width: number, height: number): string {\n if (peaks.length === 0 || width <= 0 || height <= 0) return ''\n const step = width / peaks.length\n const mid = height / 2\n const parts: string[] = []\n for (let i = 0; i < peaks.length; i += 1) {\n const peak = peaks[i] ?? 0\n const h = Math.max(0.5, peak * mid)\n const x = i * step + step / 2\n parts.push(`M${x.toFixed(2)} ${(mid - h).toFixed(2)}V${(mid + h).toFixed(2)}`)\n }\n return parts.join(' ')\n}\n\n/** 波形按比例缩放(不重算 PCM) */\nexport function normalizeWaveform(peaks: number[], length: number): number[] {\n if (peaks.length === length || peaks.length === 0) return peaks\n const out: number[] = []\n for (let i = 0; i < length; i += 1) {\n const src = Math.min(peaks.length - 1, Math.floor((i / length) * peaks.length))\n out.push(peaks[src] ?? 0)\n }\n return out\n}\n"],"ignoreList":[8],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"waveform-Ie-G5Lle.cjs","names":["createStore","produce","immer","createStore","immer","createEmptyProject","normalizeSize","remapClipsForSize","removeClipsData","asUs","trimClipData","getClipEnd","createId","duplicateClipsData","compactTrackData","createTrack","getProjectDuration"],"sources":["../../src/project/schema.ts","../../src/project/indexeddb-storage.ts","../../src/ports/web-bridge.ts","../../src/export/types.ts","../../src/errors.ts","../../src/capability.ts","../../src/editor/event-bus.ts","../../src/editor/history-store.ts","../../../../node_modules/zustand/esm/middleware/immer.mjs","../../src/editor/editor-store.ts","../../src/media/waveform.ts"],"sourcesContent":["/**\r\n * 工程序列化 / 反序列化。\r\n *\r\n * 版本号用于将来迁移。当前 schema v1:Project 原样存储,素材元数据单独存。\r\n *\r\n * 注意:素材二进制(File)不会被持久化到 IndexedDB —— 设计上只在会话内可用,\r\n * 刷新后需要重新导入源文件(提示语里明确说明)。此处仅持久化元数据与时间轴,\r\n * 保证「刷新页面后工程结构能恢复」这一验收项,并给出缺失素材的占位。\r\n */\r\n\r\nimport type { MediaAsset, Project } from '../types'\r\nimport { PROJECT_SCHEMA_VERSION } from '../constants'\r\n\r\nexport interface SerializedProject {\r\n schemaVersion: number\r\n savedAt: number\r\n project: Project\r\n /** 素材元数据;binary 缺失时仅能恢复占位卡片 */\r\n assets: MediaAsset[]\r\n}\r\n\r\nexport interface LoadResult {\r\n project: Project\r\n assets: MediaAsset[]\r\n /** 缺少二进制源的素材 id(UI 用占位显示) */\r\n missingAssetIds: string[]\r\n}\r\n\r\nexport function serializeProject(project: Project, assets: MediaAsset[]): SerializedProject {\r\n return {\r\n schemaVersion: PROJECT_SCHEMA_VERSION,\r\n savedAt: Date.now(),\r\n project,\r\n assets: assets.map(stripRuntimeFields),\r\n }\r\n}\r\n\r\n/** 去掉不可持久化 / 无意义的运行时字段 */\r\nfunction stripRuntimeFields(asset: MediaAsset): MediaAsset {\r\n const { thumbnailUrls: _thumbnailUrls, ...rest } = asset\r\n void _thumbnailUrls\r\n return { ...rest }\r\n}\r\n\r\nexport class ProjectSchemaError extends Error {\r\n constructor(message: string) {\r\n super(message)\r\n this.name = 'ProjectSchemaError'\r\n }\r\n}\r\n\r\nexport function deserializeProject(raw: unknown): LoadResult {\r\n if (raw == null || typeof raw !== 'object') {\r\n throw new ProjectSchemaError('工程数据为空或格式错误')\r\n }\r\n const data = raw as Partial<SerializedProject>\r\n if (typeof data.schemaVersion !== 'number') {\r\n throw new ProjectSchemaError('工程缺少 schemaVersion,无法判断版本')\r\n }\r\n if (data.schemaVersion > PROJECT_SCHEMA_VERSION) {\r\n throw new ProjectSchemaError(\r\n `工程版本 ${data.schemaVersion} 高于当前应用支持的 ${PROJECT_SCHEMA_VERSION},请升级应用`,\r\n )\r\n }\r\n // 迁移入口:未来版本在此按版本号逐步升级\r\n const project = migrate(data.schemaVersion, data.project)\r\n if (project == null) throw new ProjectSchemaError('工程主体缺失')\r\n\r\n const assets = Array.isArray(data.assets) ? data.assets : []\r\n const assetIds = new Set(assets.map((a) => a.id))\r\n\r\n // 两类「缺失」:\r\n // 1. 工程引用了 Assets 表里根本不存在的素材(数据损坏);\r\n // 2. 文件型素材(视频/音频/图片)—— 二进制不会被持久化,恢复后必定没有源文件,\r\n // 需要用户重新导入同名文件才能重连。\r\n const fileBacked = new Set(\r\n assets.filter((a) => a.kind === 'video' || a.kind === 'audio' || a.kind === 'image').map((a) => a.id),\r\n )\r\n const referenced = new Set(project.clips.map((c) => c.assetId))\r\n const missingAssetIds = Array.from(\r\n new Set([\r\n ...project.clips.map((c) => c.assetId).filter((id) => !assetIds.has(id)),\r\n ...Array.from(fileBacked).filter((id) => referenced.has(id)),\r\n ]),\r\n )\r\n return { project, assets, missingAssetIds }\r\n}\r\n\r\nfunction migrate(version: number, project: Project | undefined): Project | null {\r\n if (project == null) return null\r\n switch (version) {\r\n case 1:\r\n default:\r\n return project\r\n }\r\n}\r\n","/**\n * IndexedDB 工程存储(内置默认实现)。\n *\n * 单 object store,key 为命名空间字符串,value 为序列化工程。\n * 一个数据库可以容纳多个工程,靠 namespace 区分(多实例 / 多项目场景)。\n *\n * ⚠️ 浏览器环境之外(Node / SSR)构造时会静默降级为内存存储,\n * 调用方不需要自己做可用性判断。\n */\n\nimport type { SerializedProject } from './schema'\nimport type { StoragePort } from './storage'\n\nexport interface IndexedDbStorageOptions {\n /** 数据库名,默认 `cut-workbench` */\n dbName?: string\n /** 数据库版本,结构变更时递增 */\n version?: number\n}\n\nconst STORE_PROJECTS = 'projects'\n\nfunction isIndexedDbAvailable(): boolean {\n try {\n return typeof indexedDB !== 'undefined' && indexedDB != null\n } catch {\n return false\n }\n}\n\nexport function createIndexedDbStorage(options: IndexedDbStorageOptions = {}): StoragePort {\n const dbName = options.dbName ?? 'cut-workbench'\n const version = options.version ?? 1\n\n // Node / 非安全上下文:退化为内存实现,接口保持一致\n if (!isIndexedDbAvailable()) {\n return createMemoryStorage()\n }\n\n let dbPromise: Promise<IDBDatabase> | null = null\n\n function openDb(): Promise<IDBDatabase> {\n dbPromise ??= new Promise<IDBDatabase>((resolve, reject) => {\n const request = indexedDB.open(dbName, version)\n request.onupgradeneeded = () => {\n const db = request.result\n if (!db.objectStoreNames.contains(STORE_PROJECTS)) {\n db.createObjectStore(STORE_PROJECTS)\n }\n }\n request.onsuccess = () => resolve(request.result)\n request.onerror = () => {\n dbPromise = null\n reject(request.error ?? new Error('IndexedDB 打开失败'))\n }\n })\n return dbPromise\n }\n\n function withStore<T>(\n mode: IDBTransactionMode,\n fn: (store: IDBObjectStore) => IDBRequest<T>,\n ): Promise<T> {\n return openDb().then(\n (db) =>\n new Promise<T>((resolve, reject) => {\n const tx = db.transaction(STORE_PROJECTS, mode)\n const store = tx.objectStore(STORE_PROJECTS)\n const request = fn(store)\n request.onsuccess = () => resolve(request.result)\n request.onerror = () => reject(request.error ?? new Error('IndexedDB 操作失败'))\n tx.onerror = () => reject(tx.error ?? new Error('IndexedDB 事务失败'))\n }),\n )\n }\n\n return {\n async load(namespace) {\n const result = await withStore<SerializedProject | undefined>('readonly', (store) =>\n store.get(namespace),\n )\n return result ?? null\n },\n\n async save(namespace, data) {\n try {\n await withStore('readwrite', (store) => store.put(data, namespace))\n return { ok: true }\n } catch (err) {\n return { ok: false, error: err instanceof Error ? err.message : String(err) }\n }\n },\n\n async remove(namespace) {\n await withStore('readwrite', (store) => store.delete(namespace))\n },\n\n async list() {\n const keys = await withStore<IDBValidKey[]>('readonly', (store) => store.getAllKeys())\n return keys.map(String)\n },\n }\n}\n\n/**\n * 内存存储。\n *\n * 用于:SSR / Node 环境兜底、单元测试、以及「只要内存态不要持久化」的宿主。\n */\nexport function createMemoryStorage(): StoragePort {\n const map = new Map<string, SerializedProject>()\n return {\n async load(namespace) {\n const value = map.get(namespace)\n // 深拷贝,避免调用方改到存储内部的状态\n return value == null ? null : structuredClone(value)\n },\n async save(namespace, data) {\n map.set(namespace, structuredClone(data))\n return { ok: true }\n },\n async remove(namespace) {\n map.delete(namespace)\n },\n async list() {\n return [...map.keys()]\n },\n }\n}\n","/**\n * 浏览器内置宿主桥(默认实现)。\n *\n * 没有传入自定义 bridge 时使用:\n * - `pickFiles` 创建一个隐藏的 `<input type=file>`,用完即弃;\n * - `saveBlob` 走 `<a download>`;\n * - 不实现 `notify`(见文件末尾注释)。\n *\n * 本文件只在浏览器里被调用,不做 SSR 保护(调用方需保证不提前触发)。\n */\n\nimport type { HostBridge, PickFilesResult } from '../ports/host-bridge'\nimport type { SaveFileResult } from '../ports/host-bridge'\nimport type { SerializedProject } from '../project/schema'\n\nexport interface WebBridgeOptions {\n /** 覆盖下载文件名(例如加前缀) */\n downloadName?: (suggested: string) => string\n}\n\n/** 触发浏览器下载;返回是否成功发起 */\nexport function downloadBlob(blob: Blob, filename: string): void {\n const url = URL.createObjectURL(blob)\n const link = document.createElement('a')\n link.href = url\n link.download = filename\n document.body.appendChild(link)\n link.click()\n link.remove()\n // 立即 revoke 会让部分浏览器来不及下载,延迟释放\n window.setTimeout(() => URL.revokeObjectURL(url), 10_000)\n}\n\nexport function createWebBridge(options: WebBridgeOptions = {}): HostBridge {\n return {\n platform: 'web',\n\n files: {\n async pickFiles(pickOptions = {}): Promise<PickFilesResult> {\n return new Promise<File[]>((resolve) => {\n const input = document.createElement('input')\n input.type = 'file'\n input.multiple = pickOptions.multiple ?? true\n if (pickOptions.accept != null && pickOptions.accept.length > 0) {\n input.accept = pickOptions.accept.join(',')\n }\n input.style.display = 'none'\n document.body.appendChild(input)\n input.addEventListener(\n 'change',\n () => {\n const files = Array.from(input.files ?? [])\n input.remove()\n resolve(files)\n },\n { once: true },\n )\n // 用户取消时 change 不触发;靠 window focus 兜底,避免输入框永久挂载\n window.addEventListener(\n 'focus',\n () => {\n window.setTimeout(() => {\n if (document.body.contains(input)) {\n input.remove()\n resolve([])\n }\n }, 300)\n },\n { once: true },\n )\n input.click()\n })\n },\n\n async saveBlob(blob, suggestedName): Promise<SaveFileResult> {\n const name = options.downloadName?.(suggestedName) ?? suggestedName\n downloadBlob(blob, name)\n return { ok: true }\n },\n\n async openProject(): Promise<SerializedProject | null> {\n const files = await this.pickFiles!({ accept: ['.json'], multiple: false })\n // 内置实现(同文件里的 pickFiles)永不返回 null,但类型上是三态;\n // 用 `?? []` 兼容自定义实现返回 null 的情况(视为用户取消)。\n const file = (files ?? [])[0]\n if (file == null) return null\n const text = await file.text()\n return JSON.parse(text) as SerializedProject\n },\n },\n\n // ⚠️ 刻意**不**实现 `notify`。\n //\n // `notify` 存在与否是「宿主是否接管提示」的唯一信号:UI 层据此决定\n // 走宿主还是弹库内 toast。内置桥如果也提供一个「打 console 的 notify」,\n // 这个信号就永远为真 —— 结果是独立使用时提示全被打进 console,\n // 库内 toast 再也不出现(用户什么也看不到)。\n //\n // console 兜底放在 `@cut-workbench/workbench` 的 lib/notify.ts 里,\n // 那里能区分「宿主接管 / 没有宿主」两种情况。\n log(level, message, meta) {\n console[level](`[cut-workbench] ${message}`, meta ?? '')\n },\n }\n}\n","/**\n * 导出能力的公开契约(纯类型 + 纯函数,不依赖 WebAV / React)。\n *\n * 放在根入口可达的位置,宿主可以只依赖 `@cut-workbench/core` 就能标注\n * 导出请求与结果,不必为了类型去加载引擎子入口。\n */\n\nimport type { ExportSettings, Project } from '../types'\n\nexport interface ExportRequest {\n /** 输出参数;缺省为 1080p / 30fps / 8Mbps */\n settings?: Partial<ExportSettings>\n /** 输出 blob 的 MIME,默认 `video/mp4` */\n mimeType?: string\n /** 输出文件名(不含路径),默认按时间生成 */\n filename?: string\n}\n\nexport interface ExportResult {\n blob: Blob\n filename: string\n mimeType: string\n /** 成片时长(微秒)。宿主入库需要 */\n durationUs: number\n width: number\n height: number\n fps: number\n bitrate: number\n elapsedMs: number\n /** 实际执行路径:Worker 或主线程回退(诊断用,宿主可忽略) */\n mode: 'worker' | 'main'\n /** 被跳过的片段(缺源文件 / 类型不支持) */\n skipped: Array<{ clipId: string; reason: string }>\n}\n\nexport interface ExportHandle {\n promise: Promise<ExportResult>\n cancel(): void\n}\n\n/** 导出默认值:1080p / 30fps / 8Mbps / 含音轨 */\nexport const DEFAULT_EXPORT_SETTINGS: ExportSettings = {\n preset: '1080p',\n width: 1920,\n height: 1080,\n fps: 30,\n bitrate: 8_000_000,\n includeAudio: true,\n}\n\nfunction even(n: number): number {\n return Math.max(2, Math.round(n / 2) * 2)\n}\n\n/**\n * 把宿主传入的局部设置补全为完整设置。\n *\n * 只给了 `preset` 时按预设推导尺寸(`source` 取工程尺寸并向下对齐到偶数),\n * 显式给了 `width` / `height` 则以显式值为准。\n */\nexport function resolveExportSettings(\n project: Project,\n partial?: Partial<ExportSettings>,\n): ExportSettings {\n const preset = partial?.preset ?? DEFAULT_EXPORT_SETTINGS.preset\n const presetSize =\n preset === '720p'\n ? { width: 1280, height: 720 }\n : preset === '1080p'\n ? { width: 1920, height: 1080 }\n : { width: even(project.width), height: even(project.height) }\n\n return {\n preset,\n width: partial?.width ?? presetSize.width,\n height: partial?.height ?? presetSize.height,\n fps: partial?.fps ?? DEFAULT_EXPORT_SETTINGS.fps,\n bitrate: partial?.bitrate ?? DEFAULT_EXPORT_SETTINGS.bitrate,\n includeAudio: partial?.includeAudio ?? DEFAULT_EXPORT_SETTINGS.includeAudio,\n }\n}\n\n/** 默认文件名(与编辑器无关,宿主可覆盖) */\nexport function createExportFilename(date = new Date()): string {\n const pad = (n: number) => String(n).padStart(2, '0')\n return `cut-test-${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}-${pad(\n date.getHours(),\n )}${pad(date.getMinutes())}${pad(date.getSeconds())}.mp4`\n}\n","/**\n * 对外错误类型。\n *\n * 放在根入口(不依赖 WebAV / React),宿主可以 `instanceof` 判断后决定\n * 是否弹错误框 —— 用户主动取消导出不是错误,不该被当成失败提示。\n */\n\n/** 导出被 `cancel()` 取消。 */\nexport class ExportCancelledError extends Error {\n constructor(message = '导出已取消') {\n super(message)\n this.name = 'ExportCancelledError'\n }\n}\n\n/** 判断任意异常是否是「用户取消导出」。 */\nexport function isExportCancelled(error: unknown): error is ExportCancelledError {\n return error instanceof ExportCancelledError\n}\n","/**\n * 运行环境能力探测。\n *\n * ⚠️ WebAV 的 `opfs-tools` 在模块加载期就访问 `navigator.storage.getDirectory()`,\n * 因此任何 `import '@webav/*'` 之前都必须先探测,否则在无 OPFS 的环境会直接白屏。\n * 本文件刻意 **不** 静态 import WebAV,调用方应当先 `detectEnvironment()`。\n */\n\n/** 缺失引擎依赖时给出的安装命令(阻断页与 console 共用同一份文案) */\nexport const ENGINE_DEPENDENCIES_INSTALL_COMMAND =\n 'npm i @webav/av-cliper @webav/av-canvas @webav/av-recorder'\n\nexport interface CapabilityReport {\n /** WebCodecs 四个核心接口是否齐全 */\n webCodecs: boolean\n /** H.264 编码是否受支持(Combinator.isSupported 的简化替代) */\n videoEncode: boolean\n /** AAC 音频编码是否受支持 */\n audioEncode: boolean\n /** OffscreenCanvas 可用 */\n offscreenCanvas: boolean\n /** OPFS 是否可用(WebAV 硬依赖) */\n opfs: boolean\n /** WebGL2 是否存在(createChromakey 需要) */\n webgl2: boolean\n /** WebAV 提供者 Combinator.isSupported 的最终结论 */\n composerSupported: boolean\n /**\n * 引擎依赖能否解析到真实模块。\n *\n * 依赖缺失属于「装包问题」而不是「浏览器不支持」,因此在报告里单列 ——\n * 阻断页要给出可执行的安装命令,而不是让人去换浏览器。\n */\n dependencies: {\n '@webav/av-cliper': boolean\n '@webav/av-canvas': boolean\n }\n /** 缺失的 npm 包名;空数组表示齐全。阻断页据此渲染安装命令 */\n missingDependencies: string[]\n /** 逐项人类可读说明,失败项会出现在阻断页上 */\n issues: string[]\n /** 建议 */\n advice: string\n}\n\nfunction hasWebCodecs(): boolean {\n return (\n typeof globalThis.VideoEncoder !== 'undefined' &&\n typeof globalThis.VideoDecoder !== 'undefined' &&\n typeof globalThis.AudioEncoder !== 'undefined' &&\n typeof globalThis.AudioDecoder !== 'undefined' &&\n typeof globalThis.VideoFrame !== 'undefined' &&\n typeof globalThis.AudioData !== 'undefined'\n )\n}\n\nfunction hasWebGL2(): boolean {\n try {\n const canvas = document.createElement('canvas')\n return canvas.getContext('webgl2') != null\n } catch {\n return false\n }\n}\n\nexport async function detectOpfs(): Promise<boolean> {\n try {\n if (typeof navigator === 'undefined' || navigator.storage?.getDirectory == null) {\n return false\n }\n await navigator.storage.getDirectory()\n return true\n } catch {\n return false\n }\n}\n\n/**\n * 探测引擎依赖是否真的可解析。\n *\n * 三种失败形态都要覆盖,且都必须**不抛错**:\n * 1. 运行时 `Cannot find module` —— 动态 import 直接 reject;\n * 2. 构建期缺失(npm 的 optional peer 桩模块)—— import 成功但模块是空的,\n * 期望的导出一律 `undefined`;\n * 3. 版本不匹配 —— 导出存在但不是预期的形状。\n *\n * 因此判定依据是「关键导出存在」而不是「import 成功」。\n */\nasync function probeDependency(\n load: () => Promise<Record<string, unknown>>,\n requiredExports: string[],\n): Promise<boolean> {\n try {\n const mod = await load()\n // 桩模块在 Vite 里是 `export default {}` 形态,具名导出全是 undefined\n const target = (mod as { default?: Record<string, unknown> }).default ?? mod\n return requiredExports.every((key) => (target as Record<string, unknown>)[key] != null)\n } catch {\n return false\n }\n}\n\nexport async function detectDependencies(): Promise<CapabilityReport['dependencies']> {\n const [cliper, canvas] = await Promise.all([\n probeDependency(() => import('@webav/av-cliper'), ['Combinator']),\n probeDependency(() => import('@webav/av-canvas'), ['AVCanvas']),\n ])\n return { '@webav/av-cliper': cliper, '@webav/av-canvas': canvas }\n}\n\n/**\n * 探测当前浏览器是否满足 WebAV 运行条件。\n *\n * 顺序是有意为之:先查依赖是否装上,再查浏览器是否支持。\n * 依赖缺失时**不再**尝试 import WebAV —— 桩模块的 `Combinator.isSupported()`\n * 只会抛一个难以理解的 TypeError,掩盖真正的原因(没装包)。\n */\nexport async function detectEnvironment(): Promise<CapabilityReport> {\n const issues: string[] = []\n const webCodecs = hasWebCodecs()\n const offscreenCanvas = typeof OffscreenCanvas !== 'undefined'\n const webgl2 = hasWebGL2()\n\n const dependencies = await detectDependencies()\n const missingDependencies = Object.entries(dependencies)\n .filter(([, ok]) => !ok)\n .map(([name]) => name)\n\n if (missingDependencies.length > 0) {\n issues.push(\n `缺少引擎依赖:${missingDependencies.join('、')}。` +\n `请执行 \\`${ENGINE_DEPENDENCIES_INSTALL_COMMAND}\\` 后重新构建。`,\n )\n }\n\n if (!webCodecs) {\n issues.push('当前浏览器缺少 WebCodecs(VideoEncoder / VideoDecoder / AudioEncoder)。')\n }\n if (!offscreenCanvas) {\n issues.push('当前浏览器不支持 OffscreenCanvas。')\n }\n\n const opfs = await detectOpfs()\n if (!opfs) {\n issues.push(\n 'OPFS 不可用(navigator.storage.getDirectory 访问失败)。WebAV 依赖它读写临时文件,' +\n '常见原因是隐私模式或站点数据被完全禁用。',\n )\n }\n\n let videoEncode = false\n let audioEncode = false\n let composerSupported = false\n\n if (missingDependencies.length === 0 && webCodecs && opfs) {\n try {\n const [{ Combinator }, videoSupport, audioSupport] = await Promise.all([\n import('@webav/av-cliper'),\n VideoEncoder.isConfigSupported({\n codec: 'avc1.42E032',\n width: 1920,\n height: 1080,\n bitrate: 5_000_000,\n }),\n AudioEncoder.isConfigSupported({\n codec: 'mp4a.40.2',\n sampleRate: 48000,\n numberOfChannels: 2,\n }),\n ])\n videoEncode = videoSupport.supported ?? false\n audioEncode = audioSupport.supported ?? false\n composerSupported = await Combinator.isSupported()\n if (!videoEncode) issues.push('当前设备无法编码 H.264(avc1.42E032)。')\n if (!audioEncode) issues.push('当前设备无法编码 AAC(mp4a.40.2)。')\n if (!composerSupported) issues.push('WebAV Combinator 自检未通过。')\n } catch (err) {\n issues.push(`WebAV 初始化失败:${err instanceof Error ? err.message : String(err)}`)\n }\n }\n\n // 依赖缺失和浏览器不支持是两类问题,建议也必须分开给:\n // 让人对着「请换浏览器」去修一个装包问题是最糟的体验。\n const advice =\n missingDependencies.length > 0\n ? `请先安装引擎依赖:${ENGINE_DEPENDENCIES_INSTALL_COMMAND}`\n : issues.length === 0\n ? '环境满足全部要求,可正常使用。'\n : '建议使用最新版 Chrome 或 Edge(Chromium 内核),并关闭无痕模式后重试。'\n\n return {\n webCodecs,\n videoEncode,\n audioEncode,\n offscreenCanvas,\n opfs,\n webgl2,\n composerSupported,\n dependencies,\n missingDependencies,\n issues,\n advice,\n }\n}\n\n/** 只要满足最低可用条件即允许进入编辑器 */\nexport function isEnvironmentUsable(report: CapabilityReport | null): boolean {\n if (!report) return false\n if (report.missingDependencies.length > 0) return false\n return report.opfs && report.composerSupported\n}\n\n/** 文本摘要,用于 TopBar 徽标 */\nexport function summarizeCapabilities(report: CapabilityReport | null): string {\n if (!report) return '探测中'\n if (report.missingDependencies.length > 0) return '缺少引擎依赖'\n if (report.issues.length === 0) return '环境就绪'\n return `${report.issues.length} 项异常`\n}\n","/**\r\n * 类型化事件总线。\r\n *\r\n * 为什么不用 mitt / eventemitter3:\r\n * - 需要 `emit` 时隔离监听器异常(一个订阅方抛错不能打断其他订阅方);\r\n * - 需要 `on()` 返回 unsubscribe(React effect 里直接用);\r\n * - 需要对宿主侧做类型收窄(宿主只能订阅,不能 emit)。\r\n *\r\n * 总线是**每个 Editor 实例一份**,不做模块级单例,否则多实例会互相串台。\r\n */\r\n\r\n// 用接口约束而不是 Record<string, unknown>:后者的索引签名会拒绝普通 interface\r\nexport type EventMap = object\r\n\r\nexport type Unsubscribe = () => void\r\n\r\ntype Handler<Payload> = (payload: Payload) => void\r\n\r\nexport interface EventBus<M extends EventMap> {\r\n /** 订阅;相同 handler 重复订阅会被忽略 */\r\n on<K extends keyof M & string>(type: K, handler: Handler<M[K]>): Unsubscribe\r\n /** 订阅一次 */\r\n once<K extends keyof M & string>(type: K, handler: Handler<M[K]>): Unsubscribe\r\n /** 退订;不传 handler 时清空该类型 */\r\n off<K extends keyof M & string>(type: K, handler?: Handler<M[K]>): void\r\n /** 派发;单个监听器抛错不影响其他监听器 */\r\n emit<K extends keyof M & string>(type: K, payload: M[K]): void\r\n /** 清空所有订阅(dispose 时调用) */\r\n clear(): void\r\n /** 当前某类型的订阅数(调试用) */\r\n listenerCount<K extends keyof M & string>(type: K): number\r\n}\r\n\r\nexport function createEventBus<M extends EventMap>(onError?: (error: unknown) => void): EventBus<M> {\r\n const listeners = new Map<string, Set<Handler<never>>>()\r\n\r\n function ensure(type: string): Set<Handler<never>> {\r\n let set = listeners.get(type)\r\n if (set == null) {\r\n set = new Set()\r\n listeners.set(type, set)\r\n }\r\n return set\r\n }\r\n\r\n return {\r\n on(type, handler) {\r\n const set = ensure(type)\r\n set.add(handler as Handler<never>)\r\n return () => {\r\n set.delete(handler as Handler<never>)\r\n }\r\n },\r\n\r\n once(type, handler) {\r\n const unsubscribe = this.on(type, (payload) => {\r\n unsubscribe()\r\n handler(payload)\r\n })\r\n return unsubscribe\r\n },\r\n\r\n off(type, handler) {\r\n if (handler == null) {\r\n listeners.delete(type)\r\n return\r\n }\r\n listeners.get(type)?.delete(handler as Handler<never>)\r\n },\r\n\r\n emit(type, payload) {\r\n const set = listeners.get(type)\r\n if (set == null || set.size === 0) return\r\n // 复制一份再遍历:监听器内部可能退订自己\r\n for (const handler of [...set]) {\r\n try {\r\n ;(handler as Handler<unknown>)(payload)\r\n } catch (err) {\r\n onError?.(err)\r\n }\r\n }\r\n },\r\n\r\n clear() {\r\n listeners.clear()\r\n },\r\n\r\n listenerCount(type) {\r\n return listeners.get(type)?.size ?? 0\r\n },\r\n }\r\n}\r\n\r\n/** 宿主持有的事件视图:只能订阅,不能派发 */\r\nexport type EventEmitterView<M extends EventMap> = Pick<EventBus<M>, 'on' | 'once' | 'off'>\r\n","/**\n * 撤销/重做历史栈(最多 50 步快照)。\n *\n * 与编辑器 store 解耦:这里只负责保存快照,编辑器在变更**前**调用 `record()`,\n * undo/redo 时取出快照整体替换。\n *\n * 为什么整体快照而不是命令式 patch:时间轴操作(分割/裁剪/吸附)彼此耦合,\n * 整体快照在 200 clip 量级下也只有几百 KB,换来的是绝对正确的撤销语义。\n *\n * 合并(coalescing):连续的高频同质操作(拖动滑块、画布内拖动素材)若每步都压栈,\n * 一次拖动就会产生几十条历史,用户按 Ctrl+Z 时感觉「撤销不动」。\n * 因此 `record()` 支持 `mergeKey`:同一 key 在时间窗口内的后续记录会被丢弃,\n * 栈里保留的始终是「本次操作开始前」的快照 —— 一次拖动 = 一步撤销。\n *\n * 使用 zustand 的 **vanilla** 版本创建:不绑定 React,Core 依旧零 React 依赖。\n */\n\nimport { createStore, type StoreApi } from 'zustand/vanilla'\nimport type { MediaAsset, Project } from '../types'\nimport { HISTORY_MERGE_WINDOW_MS, MAX_HISTORY_STEPS } from '../constants'\n\nexport interface EditorSnapshot {\n project: Project\n assets: MediaAsset[]\n /** 该快照产生的原因,便于调试与 UI 提示 */\n label?: string\n}\n\nexport interface RecordOptions {\n label?: string\n /**\n * 合并键。相同 key 在 `HISTORY_MERGE_WINDOW_MS` 内的连续记录会被合并为一步。\n *\n * 约定格式:`${操作类型}:${目标 id}`,例如 `slider:clip_xx:opacity`。\n * 不传则每次都是独立一步。\n */\n mergeKey?: string\n}\n\nexport interface HistoryState {\n past: EditorSnapshot[]\n future: EditorSnapshot[]\n /** 上一条记录的合并键(用于同一会话内的连续合并) */\n lastMergeKey: string | null\n /** 上一条记录的写入时间 */\n lastRecordedAt: number\n record: (snapshot: EditorSnapshot, options?: RecordOptions) => void\n undo: (current: EditorSnapshot) => EditorSnapshot | null\n redo: (current: EditorSnapshot) => EditorSnapshot | null\n clear: () => void\n /** 结束当前合并会话:之后即使 mergeKey 相同也会新起一步 */\n endMerge: () => void\n canUndo: () => boolean\n canRedo: () => boolean\n}\n\nexport type HistoryStore = StoreApi<HistoryState>\n\nfunction cloneSnapshot(snapshot: EditorSnapshot): EditorSnapshot {\n return {\n project: structuredClone(snapshot.project),\n assets: structuredClone(snapshot.assets),\n label: snapshot.label,\n }\n}\n\n/** 每个 Editor 实例一份,不复用 */\nexport function createHistoryStore(): HistoryStore {\n return createStore<HistoryState>()((set, get) => ({\n past: [],\n future: [],\n lastMergeKey: null,\n lastRecordedAt: 0,\n\n record: (snapshot, options) => {\n const state = get()\n const mergeKey = options?.mergeKey ?? null\n const now = Date.now()\n\n // 连续同质操作:保留「第一次操作前」的快照,丢弃后续的,从而实现一步撤销\n if (\n mergeKey != null &&\n mergeKey === state.lastMergeKey &&\n now - state.lastRecordedAt < HISTORY_MERGE_WINDOW_MS\n ) {\n set({ lastRecordedAt: now, future: [] })\n return\n }\n\n const labeled: EditorSnapshot = options?.label\n ? { ...snapshot, label: options.label }\n : snapshot\n\n set((current) => {\n const past = [...current.past, cloneSnapshot(labeled)]\n return {\n past: past.length > MAX_HISTORY_STEPS ? past.slice(past.length - MAX_HISTORY_STEPS) : past,\n future: [],\n lastMergeKey: mergeKey,\n lastRecordedAt: now,\n }\n })\n },\n\n undo: (current) => {\n const { past } = get()\n const previous = past[past.length - 1]\n if (!previous) return null\n set((state) => ({\n past: state.past.slice(0, -1),\n future: [...state.future, cloneSnapshot(current)],\n // 撤销后必须断开合并:否则紧接着的同类编辑会错误地并进上一步\n lastMergeKey: null,\n lastRecordedAt: 0,\n }))\n return previous\n },\n\n redo: (current) => {\n const { future } = get()\n const next = future[future.length - 1]\n if (!next) return null\n set((state) => ({\n future: state.future.slice(0, -1),\n past: [...state.past, cloneSnapshot(current)],\n lastMergeKey: null,\n lastRecordedAt: 0,\n }))\n return next\n },\n\n clear: () => set({ past: [], future: [], lastMergeKey: null, lastRecordedAt: 0 }),\n\n endMerge: () => set({ lastMergeKey: null, lastRecordedAt: 0 }),\n\n canUndo: () => get().past.length > 0,\n canRedo: () => get().future.length > 0,\n }))\n}\n","import { produce } from 'immer';\n\nconst immerImpl = (initializer) => (set, get, store) => {\n store.setState = (updater, replace, ...args) => {\n const nextState = typeof updater === \"function\" ? produce(updater) : updater;\n return set(nextState, replace, ...args);\n };\n return initializer(store.setState, get, store);\n};\nconst immer = immerImpl;\n\nexport { immer };\n","/**\n * 编辑器主状态(zustand vanilla + immer)。\n *\n * 分三层:store 依赖 core,core 不依赖 React;UI 侧通过 `zustand/react` 或\n * `useSyncExternalStore` 绑定。\n *\n * 时间单位统一微秒;`selection` / `playheadUs` 属于 UI 状态但和编辑强相关,放这里。\n *\n * 历史记录由独立的 history store 统一管理:每次「结构性变更」前把当前快照压栈。\n * 高频交互(拖拽过程)传 `recordHistory = false`,只在松手时记一次。\n *\n * ⚠️ 使用工厂而非模块级单例:同一页面挂载两个工作台、或 Electron 多窗口时,\n * 每个实例必须拥有独立的 store,否则会共享工程数据。\n */\n\nimport { createStore, type StoreApi } from 'zustand/vanilla'\nimport { immer } from 'zustand/middleware/immer'\nimport type { MediaAsset, Project, TimelineClip, Track } from '../types'\nimport { createEmptyProject, createTrack, getClipEnd, getProjectDuration } from '../timeline/model'\nimport {\n compactTrackData,\n duplicateClipsData,\n removeClipsData,\n trimClipData,\n} from '../timeline/ops'\nimport { asUs } from '../timeline/time'\nimport { normalizeSize, remapClipsForSize } from '../timeline/canvas-size'\nimport { createId } from '../lib/id'\nimport type { HistoryStore } from './history-store'\n\nexport interface EditorState {\n project: Project\n assets: MediaAsset[]\n selectedClipIds: string[]\n selectedAssetId: string | null\n playheadUs: number\n playing: boolean\n masterVolume: number\n dirty: boolean\n /**\n * 缺少源文件的素材 id。\n *\n * 素材二进制不会被持久化(File 无法存进 IndexedDB),因此刷新后恢复的工程里\n * clip 仍存在但素材已不可用。UI 据此渲染「缺失素材」占位,并静默跳过预览,\n * 而不是每个 clip 都弹一次错误。\n */\n missingAssetIds: string[]\n\n setProject: (project: Project, assets: MediaAsset[], missingAssetIds?: string[]) => void\n resetProject: () => void\n renameProject: (name: string) => void\n setProjectSize: (width: number, height: number, mode?: 'scale' | 'keep') => void\n /** 运行期改工程 id(宿主把临时工程归入某项目时用) */\n setProjectId: (id: string) => void\n /** 整体替换「缺失素材」列表(importJson 补齐二进制后回写) */\n setMissingAssetIds: (ids: string[]) => void\n\n addAsset: (asset: MediaAsset) => void\n clearMissing: (assetId: string) => void\n replaceAsset: (assetId: string, next: MediaAsset) => void\n updateAsset: (assetId: string, patch: Partial<MediaAsset>) => void\n removeAsset: (assetId: string) => void\n\n addClip: (clip: TimelineClip) => void\n addClips: (clips: TimelineClip[]) => void\n /**\n * 修改片段属性。\n *\n * @param recordHistoryFlag 是否记录历史(拖拽过程可传 false)\n * @param mergeKey 合并键;拖动滑块这类高频连续修改必须传,\n * 否则一次拖动会产生几十条撤销记录。\n */\n updateClip: (\n clipId: string,\n patch: Partial<TimelineClip>,\n recordHistoryFlag?: boolean,\n mergeKey?: string,\n ) => void\n updateClips: (patches: Array<{ id: string; patch: Partial<TimelineClip> }>) => void\n removeClips: (ids: string[]) => void\n moveClip: (clipId: string, trackId: string, startUs: number, recordHistory?: boolean) => void\n trimClip: (clipId: string, edge: 'start' | 'end', deltaUs: number, recordHistory?: boolean) => void\n splitClipsAt: (atUs: number, clipIds?: string[]) => string[]\n duplicateSelection: (offsetUs: number) => Array<{ sourceId: string; newId: string }>\n compactTrack: (trackId: string) => void\n\n addTrack: (kind: Track['kind']) => string\n removeTrack: (trackId: string) => void\n updateTrack: (trackId: string, patch: Partial<Track>) => void\n\n selectClips: (ids: string[]) => void\n toggleSelectClip: (id: string) => void\n selectAsset: (id: string | null) => void\n setPlayhead: (us: number) => void\n setPlaying: (playing: boolean) => void\n setMasterVolume: (v: number) => void\n\n undo: () => void\n redo: () => void\n markSaved: () => void\n}\n\nexport type EditorStore = StoreApi<EditorState>\n\nexport interface CreateEditorStoreOptions {\n history: HistoryStore\n project?: Project\n assets?: MediaAsset[]\n}\n\n/**\n * 创建编辑器 store。\n *\n * @param options.history 与之配对的历史栈(每个编辑器一个,不要共享)\n */\nexport function createEditorStore(options: CreateEditorStoreOptions): EditorStore {\n const history = options.history\n const store = createStore<EditorState>()(\n immer((set, get) => {\n /** 在结构性变更前压栈(必须在 set 之前调用,拿到的是「变更前」的完整快照) */\n function recordHistory(label: string, mergeKey?: string): void {\n const { project, assets } = get()\n history.getState().record(\n {\n project: structuredClone(project),\n assets: structuredClone(assets),\n label,\n },\n mergeKey != null ? { label, mergeKey } : { label },\n )\n }\n\n return {\n project: options.project ?? createEmptyProject(),\n assets: options.assets ?? [],\n selectedClipIds: [],\n selectedAssetId: null,\n playheadUs: 0,\n playing: false,\n masterVolume: 1,\n dirty: false,\n missingAssetIds: [],\n\n setProject: (project, assets, missingAssetIds = []) =>\n set((draft) => {\n draft.project = project\n draft.assets = assets\n draft.missingAssetIds = missingAssetIds\n draft.selectedClipIds = []\n draft.selectedAssetId = null\n draft.playheadUs = 0\n draft.dirty = false\n }),\n\n resetProject: () =>\n set((draft) => {\n draft.project = createEmptyProject()\n draft.assets = []\n draft.missingAssetIds = []\n draft.selectedClipIds = []\n draft.selectedAssetId = null\n draft.playheadUs = 0\n draft.playing = false\n draft.dirty = false\n }),\n\n renameProject: (name) => {\n recordHistory('重命名工程')\n set((draft) => {\n draft.project.name = name\n draft.project.updatedAt = Date.now()\n draft.dirty = true\n })\n },\n\n setProjectId: (id) => {\n if (id === get().project.id) return\n recordHistory('修改工程 ID')\n set((draft) => {\n draft.project.id = id\n draft.project.updatedAt = Date.now()\n draft.dirty = true\n })\n },\n\n setMissingAssetIds: (ids) =>\n set((draft) => {\n draft.missingAssetIds = [...ids]\n }),\n\n /**\n * 修改画布尺寸(比例)。\n *\n * 素材的 `transform` 以工程分辨率为坐标系,因此换尺寸时必须同步重映射,\n * 否则「铺满画面」的素材在新画布里会偏到一角甚至完全跑到画外。\n *\n * @param mode `scale`(默认)素材随画布等比缩放保持构图;`keep` 素材坐标不变\n */\n setProjectSize: (width, height, mode = 'scale') => {\n const current = get().project\n const next = normalizeSize(width, height)\n if (next.width === current.width && next.height === current.height) return\n recordHistory('修改画布尺寸')\n set((draft) => {\n const from = { width: draft.project.width, height: draft.project.height }\n draft.project.width = next.width\n draft.project.height = next.height\n draft.project.clips = remapClipsForSize(\n draft.project.clips,\n from,\n next,\n mode,\n ) as typeof draft.project.clips\n draft.project.updatedAt = Date.now()\n draft.dirty = true\n })\n },\n\n addAsset: (asset) =>\n set((draft) => {\n draft.assets.push(asset)\n draft.dirty = true\n }),\n\n /** 重新导入素材后,解除「缺失」标记(按 id 匹配) */\n clearMissing: (assetId) =>\n set((draft) => {\n draft.missingAssetIds = draft.missingAssetIds.filter((id) => id !== assetId)\n }),\n\n /**\n * 用新导入的素材替换旧记录(保留原 id)。\n *\n * 用于「刷新后重新导入同名文件」的重连:assetId 不变,时间轴上引用它的 clip\n * 无需改动即可恢复。二进制替换由 ClipRegistry.replaceAssetSource 负责。\n */\n replaceAsset: (assetId, next) =>\n set((draft) => {\n const index = draft.assets.findIndex((a) => a.id === assetId)\n if (index < 0) return\n draft.assets[index] = next\n draft.project.updatedAt = Date.now()\n draft.dirty = true\n }),\n\n updateAsset: (assetId, patch) =>\n set((draft) => {\n const asset = draft.assets.find((a) => a.id === assetId)\n if (asset) Object.assign(asset, patch)\n draft.dirty = true\n }),\n\n removeAsset: (assetId) => {\n recordHistory('删除素材')\n set((draft) => {\n draft.assets = draft.assets.filter((a) => a.id !== assetId)\n const removed = new Set(\n draft.project.clips.filter((c) => c.assetId === assetId).map((c) => c.id),\n )\n draft.project.clips = draft.project.clips.filter((c) => !removed.has(c.id))\n draft.selectedClipIds = draft.selectedClipIds.filter((id) => !removed.has(id))\n if (draft.selectedAssetId === assetId) draft.selectedAssetId = null\n draft.project.updatedAt = Date.now()\n draft.dirty = true\n })\n },\n\n addClip: (clip) => {\n recordHistory('添加片段')\n set((draft) => {\n draft.project.clips.push(clip)\n draft.selectedClipIds = [clip.id]\n draft.project.updatedAt = Date.now()\n draft.dirty = true\n })\n },\n\n addClips: (clips) => {\n if (clips.length === 0) return\n recordHistory('添加片段')\n set((draft) => {\n draft.project.clips.push(...clips)\n draft.selectedClipIds = clips.map((c) => c.id)\n draft.project.updatedAt = Date.now()\n draft.dirty = true\n })\n },\n\n updateClip: (clipId, patch, recordHistoryFlag = true, mergeKey) => {\n if (recordHistoryFlag) recordHistory('修改片段属性', mergeKey)\n set((draft) => {\n const clip = draft.project.clips.find((c) => c.id === clipId)\n if (clip) Object.assign(clip, patch)\n draft.project.updatedAt = Date.now()\n draft.dirty = true\n })\n },\n\n updateClips: (patches) => {\n recordHistory('批量修改片段')\n set((draft) => {\n for (const { id, patch } of patches) {\n const clip = draft.project.clips.find((c) => c.id === id)\n if (clip) Object.assign(clip, patch)\n }\n draft.project.updatedAt = Date.now()\n draft.dirty = true\n })\n },\n\n removeClips: (ids) => {\n if (ids.length === 0) return\n recordHistory('删除片段')\n set((draft) => {\n draft.project.clips = removeClipsData(draft.project.clips, ids)\n draft.selectedClipIds = draft.selectedClipIds.filter((id) => !ids.includes(id))\n draft.project.updatedAt = Date.now()\n draft.dirty = true\n })\n },\n\n moveClip: (clipId, trackId, startUs, recordHistoryFlag = true) => {\n if (recordHistoryFlag) recordHistory('移动片段')\n set((draft) => {\n const clip = draft.project.clips.find((c) => c.id === clipId)\n if (!clip) return\n clip.trackId = trackId\n clip.startUs = asUs(Math.max(0, startUs))\n draft.project.updatedAt = Date.now()\n draft.dirty = true\n })\n },\n\n trimClip: (clipId, edge, deltaUs, recordHistoryFlag = true) => {\n if (recordHistoryFlag) recordHistory('裁剪片段')\n set((draft) => {\n const index = draft.project.clips.findIndex((c) => c.id === clipId)\n const clip = draft.project.clips[index]\n if (!clip) return\n draft.project.clips[index] = trimClipData(clip, edge, deltaUs)\n draft.project.updatedAt = Date.now()\n draft.dirty = true\n })\n },\n\n splitClipsAt: (atUs, clipIds) => {\n const state = get()\n const targets = state.project.clips.filter(\n (c) =>\n (clipIds == null || clipIds.includes(c.id)) &&\n atUs > c.startUs &&\n atUs < getClipEnd(c),\n )\n if (targets.length === 0) return []\n\n // ⚠️ 关键:右半段必须在进入 immer draft 之前算好。\n // draft 里的对象是 Proxy,`structuredClone(proxy)` 会抛 DataCloneError。\n const patches = targets.map((clip) => {\n const end = getClipEnd(clip)\n const leftDuration = atUs - clip.startUs\n const rightDuration = end - atUs\n const sourceOffset = Math.round(leftDuration * clip.playbackRate)\n const right: TimelineClip = {\n ...structuredClone(clip),\n id: createId('clip'),\n startUs: asUs(atUs),\n durationUs: asUs(rightDuration),\n inPointUs: asUs(clip.inPointUs + sourceOffset),\n fadeInUs: asUs(Math.min(clip.fadeInUs, rightDuration)),\n }\n return {\n leftId: clip.id,\n leftDuration: asUs(leftDuration),\n leftFadeOut: asUs(Math.min(clip.fadeOutUs, leftDuration)),\n right,\n }\n })\n\n recordHistory('分割片段')\n const newIds = patches.map((p) => p.right.id)\n set((draft) => {\n for (const patch of patches) {\n const left = draft.project.clips.find((c) => c.id === patch.leftId)\n if (left) {\n left.durationUs = patch.leftDuration\n left.fadeOutUs = patch.leftFadeOut\n }\n draft.project.clips.push(patch.right)\n }\n draft.selectedClipIds = newIds\n draft.project.updatedAt = Date.now()\n draft.dirty = true\n })\n return newIds\n },\n\n duplicateSelection: (offsetUs) => {\n const state = get()\n const ids = state.selectedClipIds\n const created: Array<{ sourceId: string; newId: string }> = []\n if (ids.length === 0) return created\n\n // 同样在 draft 外计算副本(避免 structuredClone proxy)\n const pairs = duplicateClipsData(\n structuredClone(state.project.clips),\n ids,\n offsetUs,\n () => createId('clip'),\n )\n recordHistory('复制片段')\n set((draft) => {\n for (const { sourceId, clip } of pairs) {\n draft.project.clips.push(clip)\n created.push({ sourceId, newId: clip.id })\n }\n draft.selectedClipIds = created.map((c) => c.newId)\n draft.project.updatedAt = Date.now()\n draft.dirty = true\n })\n return created\n },\n\n compactTrack: (trackId) => {\n recordHistory('轨道磁吸排序')\n set((draft) => {\n draft.project.clips = compactTrackData(draft.project.clips, trackId)\n draft.project.updatedAt = Date.now()\n draft.dirty = true\n })\n },\n\n addTrack: (kind) => {\n const track = createTrack(\n kind,\n `${kind === 'visual' ? '视频' : '音频'} ${get().project.tracks.filter((t) => t.kind === kind).length + 1}`,\n )\n recordHistory('添加轨道')\n set((draft) => {\n draft.project.tracks.push(track)\n draft.project.updatedAt = Date.now()\n draft.dirty = true\n })\n return track.id\n },\n\n removeTrack: (trackId) => {\n recordHistory('删除轨道')\n set((draft) => {\n if (draft.project.tracks.length <= 1) return\n draft.project.tracks = draft.project.tracks.filter((t) => t.id !== trackId)\n draft.project.clips = draft.project.clips.filter((c) => c.trackId !== trackId)\n draft.project.updatedAt = Date.now()\n draft.dirty = true\n })\n },\n\n updateTrack: (trackId, patch) =>\n set((draft) => {\n const track = draft.project.tracks.find((t) => t.id === trackId)\n if (track) Object.assign(track, patch)\n draft.dirty = true\n }),\n\n selectClips: (ids) =>\n set((draft) => {\n draft.selectedClipIds = ids\n }),\n\n toggleSelectClip: (id) =>\n set((draft) => {\n const index = draft.selectedClipIds.indexOf(id)\n if (index >= 0) draft.selectedClipIds.splice(index, 1)\n else draft.selectedClipIds.push(id)\n }),\n\n selectAsset: (id) =>\n set((draft) => {\n draft.selectedAssetId = id\n }),\n\n setPlayhead: (us) =>\n set((draft) => {\n draft.playheadUs = asUs(Math.max(0, us))\n }),\n\n setPlaying: (playing) =>\n set((draft) => {\n draft.playing = playing\n }),\n\n setMasterVolume: (v) =>\n set((draft) => {\n draft.masterVolume = Math.max(0, Math.min(1, v))\n }),\n\n undo: () => {\n const { project, assets } = get()\n const restored = history.getState().undo({ project, assets })\n if (!restored) return\n set((draft) => {\n draft.project = restored.project\n draft.assets = restored.assets\n draft.selectedClipIds = draft.selectedClipIds.filter((id) =>\n restored.project.clips.some((c) => c.id === id),\n )\n draft.dirty = true\n })\n },\n\n redo: () => {\n const { project, assets } = get()\n const restored = history.getState().redo({ project, assets })\n if (!restored) return\n set((draft) => {\n draft.project = restored.project\n draft.assets = restored.assets\n draft.dirty = true\n })\n },\n\n markSaved: () =>\n set((draft) => {\n draft.dirty = false\n }),\n }\n }),\n )\n return store\n}\n\n// —— 细粒度 selector(避免一次拖动重渲染整个时间轴) ——\n\nexport const selectProject = (s: EditorState) => s.project\nexport const selectAssets = (s: EditorState) => s.assets\nexport const selectSelectedClipIds = (s: EditorState) => s.selectedClipIds\nexport const selectPlayheadUs = (s: EditorState) => s.playheadUs\nexport const selectProjectDuration = (s: EditorState) => getProjectDuration(s.project)\nexport const selectTracks = (s: EditorState) => s.project.tracks\nexport const selectClips = (s: EditorState) => s.project.clips\n\nexport function selectClipById(id: string) {\n return (s: EditorState) => s.project.clips.find((c) => c.id === id)\n}\n\nexport function selectAssetById(id: string | null) {\n return (s: EditorState) => (id == null ? undefined : s.assets.find((a) => a.id === id))\n}\n","/**\n * 音频波形峰值计算。\n *\n * 输入是 AudioClip 的两条 PCM 声道(Float32Array,48kHz),输出 N 个 0~1 的峰值,\n * 供时间轴 / 素材卡片绘制。\n */\n\n/** 将多声道 PCM 归并为单声道并降采样为峰值数组 */\nexport function computeWaveform(channels: Float32Array[], buckets: number): number[] {\n const left = channels[0]\n if (!left || left.length === 0 || buckets <= 0) return []\n\n const right = channels[1] ?? left\n const perBucket = Math.max(1, Math.floor(left.length / buckets))\n const peaks: number[] = []\n\n for (let b = 0; b < buckets; b += 1) {\n const start = b * perBucket\n const end = Math.min(left.length, start + perBucket)\n let peak = 0\n for (let i = start; i < end; i += 1) {\n const l = Math.abs(left[i] ?? 0)\n const r = Math.abs(right[i] ?? 0)\n const v = l > r ? l : r\n if (v > peak) peak = v\n }\n peaks.push(Math.min(1, peak))\n }\n return peaks\n}\n\n/** 生成波形 SVG path(对称柱状) */\nexport function buildWaveformPath(peaks: number[], width: number, height: number): string {\n if (peaks.length === 0 || width <= 0 || height <= 0) return ''\n const step = width / peaks.length\n const mid = height / 2\n const parts: string[] = []\n for (let i = 0; i < peaks.length; i += 1) {\n const peak = peaks[i] ?? 0\n const h = Math.max(0.5, peak * mid)\n const x = i * step + step / 2\n parts.push(`M${x.toFixed(2)} ${(mid - h).toFixed(2)}V${(mid + h).toFixed(2)}`)\n }\n return parts.join(' ')\n}\n\n/** 波形按比例缩放(不重算 PCM) */\nexport function normalizeWaveform(peaks: number[], length: number): number[] {\n if (peaks.length === length || peaks.length === 0) return peaks\n const out: number[] = []\n for (let i = 0; i < length; i += 1) {\n const src = Math.min(peaks.length - 1, Math.floor((i / length) * peaks.length))\n out.push(peaks[src] ?? 0)\n }\n return out\n}\n"],"ignoreList":[8],"mappings":"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"}