@cut-workbench/core 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunks/{canvas-size-Ct61OYjW.cjs → canvas-size-DIABzEW2.cjs} +212 -44
- package/dist/chunks/canvas-size-DIABzEW2.cjs.map +1 -0
- package/dist/chunks/{canvas-size-CM2oKApi.js → canvas-size-Dlnng74w.js} +168 -30
- package/dist/chunks/canvas-size-Dlnng74w.js.map +1 -0
- package/dist/chunks/id-CJsdlGei.js +35 -0
- package/dist/chunks/id-CJsdlGei.js.map +1 -0
- package/dist/chunks/id-DaCxD4-5.cjs +70 -0
- package/dist/chunks/id-DaCxD4-5.cjs.map +1 -0
- package/dist/chunks/{snap-DyIuO2hU.cjs → snap-Cfk15JVg.cjs} +2 -22
- package/dist/chunks/{snap-DyIuO2hU.cjs.map → snap-Cfk15JVg.cjs.map} +1 -1
- package/dist/chunks/{snap-CBV9G4xq.js → snap-DpkVPVJN.js} +3 -17
- package/dist/chunks/{snap-CBV9G4xq.js.map → snap-DpkVPVJN.js.map} +1 -1
- package/dist/chunks/thumbnails-DEjnao5r.cjs +81 -0
- package/dist/chunks/thumbnails-DEjnao5r.cjs.map +1 -0
- package/dist/chunks/thumbnails-qH0dX_Zs.js +64 -0
- package/dist/chunks/thumbnails-qH0dX_Zs.js.map +1 -0
- package/dist/chunks/{time-CnMfSVYX.cjs → time-CYEd91qN.cjs} +1 -69
- package/dist/chunks/time-CYEd91qN.cjs.map +1 -0
- package/dist/chunks/{time-C_gvkVl1.js → time-ChF1GwCy.js} +2 -34
- package/dist/chunks/time-ChF1GwCy.js.map +1 -0
- package/dist/chunks/{waveform-Ie-G5Lle.cjs → waveform-BYwDdKpC.cjs} +34 -15
- package/dist/chunks/waveform-BYwDdKpC.cjs.map +1 -0
- package/dist/chunks/{waveform-DJkRtMct.js → waveform-uyGNE01H.js} +32 -13
- package/dist/chunks/waveform-uyGNE01H.js.map +1 -0
- package/dist/engine/index.cjs +668 -76
- package/dist/engine/index.cjs.map +1 -1
- package/dist/engine/index.js +648 -57
- package/dist/engine/index.js.map +1 -1
- package/dist/engine/webav/exporter.d.ts +3 -1
- package/dist/engine/webav/import-files.d.ts +17 -2
- package/dist/engine/webav/import-pipeline.d.ts +10 -0
- package/dist/engine/webav/types.d.ts +15 -0
- package/dist/index.cjs +18 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +6 -5
- package/dist/index.js.map +1 -1
- package/dist/lib/error.d.ts +6 -0
- package/dist/media/thumbnails.d.ts +8 -0
- package/dist/project/schema.d.ts +16 -4
- package/dist/timeline/index.cjs +10 -6
- package/dist/timeline/index.d.ts +1 -0
- package/dist/timeline/index.js +4 -4
- package/dist/timeline/normalize.d.ts +28 -0
- package/dist/timeline/ops.d.ts +0 -4
- package/dist/timeline/snap.d.ts +0 -2
- package/dist/types.d.ts +14 -2
- package/package.json +1 -1
- package/dist/chunks/canvas-size-CM2oKApi.js.map +0 -1
- package/dist/chunks/canvas-size-Ct61OYjW.cjs.map +0 -1
- package/dist/chunks/import-pipeline-A8KkqtZT.cjs +0 -610
- package/dist/chunks/import-pipeline-A8KkqtZT.cjs.map +0 -1
- package/dist/chunks/import-pipeline-DLIdgrIb.js +0 -521
- package/dist/chunks/import-pipeline-DLIdgrIb.js.map +0 -1
- package/dist/chunks/time-C_gvkVl1.js.map +0 -1
- package/dist/chunks/time-CnMfSVYX.cjs.map +0 -1
- package/dist/chunks/waveform-DJkRtMct.js.map +0 -1
- package/dist/chunks/waveform-Ie-G5Lle.cjs.map +0 -1
|
@@ -50,7 +50,9 @@ export interface BuiltCombinator {
|
|
|
50
50
|
* @param instance 该 clip 独占的 IClip
|
|
51
51
|
* @param clip 时间轴数据(提供 volume / fadeIn / fadeOut)
|
|
52
52
|
*/
|
|
53
|
-
export declare function withClipGain(instance: IClip, clip: TimelineClip
|
|
53
|
+
export declare function withClipGain(instance: IClip, clip: TimelineClip, options?: {
|
|
54
|
+
trackMuted?: boolean;
|
|
55
|
+
}): IClip;
|
|
54
56
|
/**
|
|
55
57
|
* 组装 Combinator。
|
|
56
58
|
*
|
|
@@ -11,8 +11,23 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import type { StoreApi } from 'zustand/vanilla';
|
|
13
13
|
import type { MediaAsset } from '../../types';
|
|
14
|
-
import type { EditorState } from '../../editor/editor-store';
|
|
15
14
|
import type { ClipRegistry } from './clip-registry';
|
|
15
|
+
/**
|
|
16
|
+
* 导入所需的编辑器状态切片。
|
|
17
|
+
*
|
|
18
|
+
* 引擎层只需要「读素材表 / 缺失列表」与「写回素材 / 选择 / 缺失标记」,
|
|
19
|
+
* 不应依赖完整的 `EditorState`。收窄成这个端口后,editor-store 增删无关字段
|
|
20
|
+
* 不再波及引擎,也避免了引擎能读写 `dialog`、`playheadUs` 之类的 UI 状态。
|
|
21
|
+
*/
|
|
22
|
+
export type ImportTargetState = {
|
|
23
|
+
assets: MediaAsset[];
|
|
24
|
+
missingAssetIds: string[];
|
|
25
|
+
replaceAsset: (assetId: string, next: MediaAsset) => void;
|
|
26
|
+
clearMissing: (assetId: string) => void;
|
|
27
|
+
addAsset: (asset: MediaAsset) => void;
|
|
28
|
+
selectAsset: (id: string | null) => void;
|
|
29
|
+
};
|
|
30
|
+
export type ImportTargetStore = Pick<StoreApi<ImportTargetState>, 'getState'>;
|
|
16
31
|
export interface ImportFilesOptions {
|
|
17
32
|
/** 导入成功后选中第一个素材;默认 true */
|
|
18
33
|
select?: boolean;
|
|
@@ -40,4 +55,4 @@ export interface ImportFilesResult {
|
|
|
40
55
|
* - 进度由调用方通过 `onProgress` 展示(不写 store,避免高频全局重渲染);
|
|
41
56
|
* - 与「缺失源文件」的素材同名同类型时重连,复用原 assetId。
|
|
42
57
|
*/
|
|
43
|
-
export declare function importFiles(registry: ClipRegistry, store:
|
|
58
|
+
export declare function importFiles(registry: ClipRegistry, store: ImportTargetStore, files: File[], options?: ImportFilesOptions): Promise<ImportFilesResult>;
|
|
@@ -31,6 +31,16 @@ export interface ImportFileOptions {
|
|
|
31
31
|
* @throws UnsupportedFileError 类型不支持时
|
|
32
32
|
*/
|
|
33
33
|
export declare function importFile(registry: ClipRegistry, file: File, onProgress?: (progress: number) => void, options?: ImportFileOptions): Promise<ImportedAsset>;
|
|
34
|
+
/**
|
|
35
|
+
* 为一个「已有元数据」的素材重新生成派生数据(缩略图 / 波形)。
|
|
36
|
+
*
|
|
37
|
+
* 用于重新打开工程后 `resolveFile` / `reconnectAsset` 找回文件时:
|
|
38
|
+
* 元数据里不含 thumbnailUrls(那是会话内 object URL,刷新即失效),
|
|
39
|
+
* 如果不补这一步,素材卡片会一直显示图标占位 —— 用户看到的就是「资源坏了」。
|
|
40
|
+
*
|
|
41
|
+
* 失败只记警告:派生数据缺失不影响素材可用性。
|
|
42
|
+
*/
|
|
43
|
+
export declare function regenerateDerived(registry: ClipRegistry, asset: MediaAsset, file: File): Promise<MediaAsset>;
|
|
34
44
|
/** 新建文字素材(非文件来源,无需二进制) */
|
|
35
45
|
export declare function createTextAsset(params: {
|
|
36
46
|
registry: ClipRegistry;
|
|
@@ -63,6 +63,21 @@ export interface WorkerTimelinePayload {
|
|
|
63
63
|
bgColor: string;
|
|
64
64
|
/** 导出时长上限(微秒);不传则取所有 clip 的最大结束时间 */
|
|
65
65
|
maxTimeUs?: number;
|
|
66
|
+
/**
|
|
67
|
+
* 轨道快照。
|
|
68
|
+
*
|
|
69
|
+
* Worker 必须知道 `hidden` / `muted` 才能与预览保持一致:
|
|
70
|
+
* - `hidden` → 整条轨道不参与合成;
|
|
71
|
+
* - `muted` → 只丢弃音频(画面照常),与预览的「静音轨道仍有画面」一致。
|
|
72
|
+
*
|
|
73
|
+
* 旧版本载荷没有这个字段,Worker 侧按「全部可见、不静音」兜底。
|
|
74
|
+
*/
|
|
75
|
+
tracks?: Array<{
|
|
76
|
+
id: string;
|
|
77
|
+
kind: 'visual' | 'audio';
|
|
78
|
+
hidden: boolean;
|
|
79
|
+
muted: boolean;
|
|
80
|
+
}>;
|
|
66
81
|
clips: Array<{
|
|
67
82
|
clip: TimelineClip;
|
|
68
83
|
kind: MediaAsset['kind'];
|
package/dist/index.cjs
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const
|
|
3
|
-
const
|
|
4
|
-
const
|
|
5
|
-
const
|
|
2
|
+
const require_id = require("./chunks/id-DaCxD4-5.cjs");
|
|
3
|
+
const require_time = require("./chunks/time-CYEd91qN.cjs");
|
|
4
|
+
const require_canvas_size = require("./chunks/canvas-size-DIABzEW2.cjs");
|
|
5
|
+
const require_snap = require("./chunks/snap-Cfk15JVg.cjs");
|
|
6
|
+
const require_waveform = require("./chunks/waveform-BYwDdKpC.cjs");
|
|
6
7
|
const require_srt = require("./chunks/srt-DaL36mXN.cjs");
|
|
7
8
|
const require_file_reader = require("./chunks/file-reader-DfMEluGr.cjs");
|
|
8
9
|
// src/project/file-storage.ts
|
|
@@ -118,16 +119,18 @@ async function estimateQuota() {
|
|
|
118
119
|
}
|
|
119
120
|
}
|
|
120
121
|
exports.ASPECT_PRESETS = require_canvas_size.ASPECT_PRESETS;
|
|
121
|
-
exports.DEFAULT_CLIP_VOLUME =
|
|
122
|
+
exports.DEFAULT_CLIP_VOLUME = require_id.DEFAULT_CLIP_VOLUME;
|
|
122
123
|
exports.DEFAULT_EXPORT_SETTINGS = require_waveform.DEFAULT_EXPORT_SETTINGS;
|
|
123
|
-
exports.DEFAULT_STILL_DURATION_US =
|
|
124
|
+
exports.DEFAULT_STILL_DURATION_US = require_id.DEFAULT_STILL_DURATION_US;
|
|
125
|
+
exports.DEFAULT_SUBTITLE_STYLE = require_canvas_size.DEFAULT_SUBTITLE_STYLE;
|
|
126
|
+
exports.DEFAULT_TEXT_STYLE = require_canvas_size.DEFAULT_TEXT_STYLE;
|
|
124
127
|
exports.EMPTY_SNAP = require_snap.EMPTY_SNAP;
|
|
125
128
|
exports.ENGINE_DEPENDENCIES_INSTALL_COMMAND = require_waveform.ENGINE_DEPENDENCIES_INSTALL_COMMAND;
|
|
126
129
|
exports.ExportCancelledError = require_waveform.ExportCancelledError;
|
|
127
|
-
exports.HISTORY_MERGE_WINDOW_MS =
|
|
128
|
-
exports.MAX_HISTORY_STEPS =
|
|
130
|
+
exports.HISTORY_MERGE_WINDOW_MS = require_id.HISTORY_MERGE_WINDOW_MS;
|
|
131
|
+
exports.MAX_HISTORY_STEPS = require_id.MAX_HISTORY_STEPS;
|
|
129
132
|
exports.MIN_TRANSFORM_SIZE = require_canvas_size.MIN_TRANSFORM_SIZE;
|
|
130
|
-
exports.PROJECT_SCHEMA_VERSION =
|
|
133
|
+
exports.PROJECT_SCHEMA_VERSION = require_id.PROJECT_SCHEMA_VERSION;
|
|
131
134
|
exports.ProjectSchemaError = require_waveform.ProjectSchemaError;
|
|
132
135
|
exports.RESIZE_HANDLES = require_canvas_size.RESIZE_HANDLES;
|
|
133
136
|
exports.US_PER_MS = require_time.US_PER_MS;
|
|
@@ -152,7 +155,7 @@ exports.createEventBus = require_waveform.createEventBus;
|
|
|
152
155
|
exports.createExportFilename = require_waveform.createExportFilename;
|
|
153
156
|
exports.createFileStorage = createFileStorage;
|
|
154
157
|
exports.createHistoryStore = require_waveform.createHistoryStore;
|
|
155
|
-
exports.createId =
|
|
158
|
+
exports.createId = require_id.createId;
|
|
156
159
|
exports.createIndexedDbStorage = require_waveform.createIndexedDbStorage;
|
|
157
160
|
exports.createMemoryStorage = require_waveform.createMemoryStorage;
|
|
158
161
|
exports.createTrack = require_canvas_size.createTrack;
|
|
@@ -184,11 +187,14 @@ exports.isEnvironmentUsable = require_waveform.isEnvironmentUsable;
|
|
|
184
187
|
exports.isExportCancelled = require_waveform.isExportCancelled;
|
|
185
188
|
exports.isTrackAudible = require_canvas_size.isTrackAudible;
|
|
186
189
|
exports.isValidSrt = require_srt.isValidSrt;
|
|
187
|
-
exports.moveClipData = require_canvas_size.moveClipData;
|
|
188
190
|
exports.msToUs = require_time.msToUs;
|
|
189
|
-
exports.nearestSorted = require_snap.nearestSorted;
|
|
190
191
|
exports.normalizeAngle = require_canvas_size.normalizeAngle;
|
|
192
|
+
exports.normalizeAsset = require_canvas_size.normalizeAsset;
|
|
193
|
+
exports.normalizeClip = require_canvas_size.normalizeClip;
|
|
194
|
+
exports.normalizeProject = require_canvas_size.normalizeProject;
|
|
191
195
|
exports.normalizeSize = require_canvas_size.normalizeSize;
|
|
196
|
+
exports.normalizeTrack = require_canvas_size.normalizeTrack;
|
|
197
|
+
exports.normalizeTransform = require_canvas_size.normalizeTransform;
|
|
192
198
|
exports.normalizeWaveform = require_waveform.normalizeWaveform;
|
|
193
199
|
exports.parseSrt = require_srt.parseSrt;
|
|
194
200
|
exports.parseSrtDurationUs = require_srt.parseSrtDurationUs;
|
|
@@ -219,7 +225,6 @@ exports.selectProjectDuration = require_waveform.selectProjectDuration;
|
|
|
219
225
|
exports.selectSelectedClipIds = require_waveform.selectSelectedClipIds;
|
|
220
226
|
exports.selectTracks = require_waveform.selectTracks;
|
|
221
227
|
exports.serializeProject = require_waveform.serializeProject;
|
|
222
|
-
exports.splitClipData = require_canvas_size.splitClipData;
|
|
223
228
|
exports.summarizeCapabilities = require_waveform.summarizeCapabilities;
|
|
224
229
|
exports.toLocalPoint = require_canvas_size.toLocalPoint;
|
|
225
230
|
exports.toStorageFileName = toStorageFileName;
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":[],"sources":["../src/project/file-storage.ts","../src/media/opfs.ts"],"sourcesContent":["/**\n * 文件式存储适配器。\n *\n * 宿主要把工程存成本地 JSON(Electron 主进程写盘)时,只需要提供四个文本\n * 操作,不必理解 `SerializedProject` 的字段。\n *\n * 映射规则:`<dir>/<namespace>.json`。namespace 做一次文件系统安全化:\n * 路径分隔符、Windows 保留字符与控制字符替换为 `_`,避免宿主传入\n * `projects/p_1` 这类含层级的值时写到目录之外。\n */\n\nimport type { SerializedProject } from './schema'\nimport type { StoragePort } from './storage'\n\nexport interface FileIoAdapter {\n /** 文件不存在时返回 null(不要抛错) */\n readText(path: string): Promise<string | null>\n writeText(path: string, content: string): Promise<void>\n /** 文件不存在时静默成功 */\n remove(path: string): Promise<void>\n /** 目录不存在时返回空数组 */\n list(dir: string): Promise<string[]>\n}\n\nconst JSON_EXT = '.json'\n\n/** namespace → 安全文件名(不含扩展名) */\nexport function toStorageFileName(namespace: string): string {\n const safe = namespace\n // eslint-disable-next-line no-control-regex -- 需要把控制字符也替换掉\n .replace(/[\\\\/:*?\"<>|\\u0000-\\u001f]/g, '_')\n .replace(/^\\.+/, '_')\n .trim()\n return (safe.length > 0 ? safe : 'default') + JSON_EXT\n}\n\nfunction joinPath(dir: string, name: string): string {\n const separator = dir.includes('\\\\') && !dir.includes('/') ? '\\\\' : '/'\n return dir.endsWith('/') || dir.endsWith('\\\\') ? `${dir}${name}` : `${dir}${separator}${name}`\n}\n\nexport function createFileStorage(dir: string, io: FileIoAdapter): StoragePort {\n const pathFor = (namespace: string) => joinPath(dir, toStorageFileName(namespace))\n\n return {\n async load(namespace) {\n const text = await io.readText(pathFor(namespace))\n if (text == null || text.trim().length === 0) return null\n return JSON.parse(text) as SerializedProject\n },\n\n async save(namespace, data) {\n const path = pathFor(namespace)\n try {\n await io.writeText(path, JSON.stringify(data, null, 2))\n return { ok: true, path }\n } catch (err) {\n return { ok: false, path, error: err instanceof Error ? err.message : String(err) }\n }\n },\n\n async remove(namespace) {\n await io.remove(pathFor(namespace))\n },\n\n async list() {\n const names = await io.list(dir)\n return names\n .filter((name) => name.toLowerCase().endsWith(JSON_EXT))\n .map((name) => name.slice(0, -JSON_EXT.length))\n .sort()\n },\n }\n}\n","/**\n * OPFS 可用性探测与临时文件辅助。\n *\n * WebAV 内部(opfs-tools)在模块加载期就会访问 `navigator.storage.getDirectory()`,\n * 因此这里只使用原生 File System Access API,不 import opfs-tools,避免重复副作用。\n *\n * 使用策略:小文件直接 `file.stream()` 喂给 MP4Clip;只有需要随机读取的大文件\n * 才落盘到 OPFS 临时目录(WebAV 自身也会在内部做同样的事,这里只提供探测与清理)。\n */\n\nconst TEMP_DIR = 'cut-test-tmp'\n\nexport interface OpfsProbeResult {\n available: boolean\n reason?: string\n}\n\nlet probeCache: OpfsProbeResult | null = null\n\nexport async function probeOpfs(force = false): Promise<OpfsProbeResult> {\n if (probeCache && !force) return probeCache\n try {\n if (typeof navigator === 'undefined' || navigator.storage?.getDirectory == null) {\n probeCache = { available: false, reason: '浏览器未实现 File System Access API。' }\n return probeCache\n }\n await navigator.storage.getDirectory()\n probeCache = { available: true }\n } catch (err) {\n probeCache = {\n available: false,\n reason: err instanceof Error ? err.message : String(err),\n }\n }\n return probeCache\n}\n\nasync function getTempDir(): Promise<FileSystemDirectoryHandle> {\n const root = await navigator.storage.getDirectory()\n return root.getDirectoryHandle(TEMP_DIR, { create: true })\n}\n\n/** 写入临时文件,返回文件名;调用方负责后续清理 */\nexport async function writeTempFile(name: string, data: Blob | ArrayBuffer): Promise<string> {\n const dir = await getTempDir()\n const handle = await dir.getFileHandle(name, { create: true })\n const writable = await handle.createWritable()\n await writable.write(data)\n await writable.close()\n return name\n}\n\n/** 读取临时文件 */\nexport async function readTempFile(name: string): Promise<File> {\n const dir = await getTempDir()\n const handle = await dir.getFileHandle(name)\n return handle.getFile()\n}\n\nexport async function removeTempFile(name: string): Promise<void> {\n try {\n const dir = await getTempDir()\n await dir.removeEntry(name)\n } catch {\n // 文件不存在时静默忽略:清理流程不应打断主流程\n }\n}\n\n/** 清空整个临时目录 */\nexport async function clearTempFiles(): Promise<void> {\n try {\n const root = await navigator.storage.getDirectory()\n await root.removeEntry(TEMP_DIR, { recursive: true })\n } catch {\n // 目录不存在时忽略\n }\n}\n\n/** 估算可用配额(用于导出前的提示) */\nexport async function estimateQuota(): Promise<{ usage: number; quota: number } | null> {\n try {\n const estimate = await navigator.storage.estimate()\n return { usage: estimate.usage ?? 0, quota: estimate.quota ?? 0 }\n } catch {\n return null\n }\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.cjs","names":[],"sources":["../src/project/file-storage.ts","../src/media/opfs.ts"],"sourcesContent":["/**\n * 文件式存储适配器。\n *\n * 宿主要把工程存成本地 JSON(Electron 主进程写盘)时,只需要提供四个文本\n * 操作,不必理解 `SerializedProject` 的字段。\n *\n * 映射规则:`<dir>/<namespace>.json`。namespace 做一次文件系统安全化:\n * 路径分隔符、Windows 保留字符与控制字符替换为 `_`,避免宿主传入\n * `projects/p_1` 这类含层级的值时写到目录之外。\n */\n\nimport type { SerializedProject } from './schema'\nimport type { StoragePort } from './storage'\n\nexport interface FileIoAdapter {\n /** 文件不存在时返回 null(不要抛错) */\n readText(path: string): Promise<string | null>\n writeText(path: string, content: string): Promise<void>\n /** 文件不存在时静默成功 */\n remove(path: string): Promise<void>\n /** 目录不存在时返回空数组 */\n list(dir: string): Promise<string[]>\n}\n\nconst JSON_EXT = '.json'\n\n/** namespace → 安全文件名(不含扩展名) */\nexport function toStorageFileName(namespace: string): string {\n const safe = namespace\n // eslint-disable-next-line no-control-regex -- 需要把控制字符也替换掉\n .replace(/[\\\\/:*?\"<>|\\u0000-\\u001f]/g, '_')\n .replace(/^\\.+/, '_')\n .trim()\n return (safe.length > 0 ? safe : 'default') + JSON_EXT\n}\n\nfunction joinPath(dir: string, name: string): string {\n const separator = dir.includes('\\\\') && !dir.includes('/') ? '\\\\' : '/'\n return dir.endsWith('/') || dir.endsWith('\\\\') ? `${dir}${name}` : `${dir}${separator}${name}`\n}\n\nexport function createFileStorage(dir: string, io: FileIoAdapter): StoragePort {\n const pathFor = (namespace: string) => joinPath(dir, toStorageFileName(namespace))\n\n return {\n async load(namespace) {\n const text = await io.readText(pathFor(namespace))\n if (text == null || text.trim().length === 0) return null\n return JSON.parse(text) as SerializedProject\n },\n\n async save(namespace, data) {\n const path = pathFor(namespace)\n try {\n await io.writeText(path, JSON.stringify(data, null, 2))\n return { ok: true, path }\n } catch (err) {\n return { ok: false, path, error: err instanceof Error ? err.message : String(err) }\n }\n },\n\n async remove(namespace) {\n await io.remove(pathFor(namespace))\n },\n\n async list() {\n const names = await io.list(dir)\n return names\n .filter((name) => name.toLowerCase().endsWith(JSON_EXT))\n .map((name) => name.slice(0, -JSON_EXT.length))\n .sort()\n },\n }\n}\n","/**\n * OPFS 可用性探测与临时文件辅助。\n *\n * WebAV 内部(opfs-tools)在模块加载期就会访问 `navigator.storage.getDirectory()`,\n * 因此这里只使用原生 File System Access API,不 import opfs-tools,避免重复副作用。\n *\n * 使用策略:小文件直接 `file.stream()` 喂给 MP4Clip;只有需要随机读取的大文件\n * 才落盘到 OPFS 临时目录(WebAV 自身也会在内部做同样的事,这里只提供探测与清理)。\n */\n\nconst TEMP_DIR = 'cut-test-tmp'\n\nexport interface OpfsProbeResult {\n available: boolean\n reason?: string\n}\n\nlet probeCache: OpfsProbeResult | null = null\n\nexport async function probeOpfs(force = false): Promise<OpfsProbeResult> {\n if (probeCache && !force) return probeCache\n try {\n if (typeof navigator === 'undefined' || navigator.storage?.getDirectory == null) {\n probeCache = { available: false, reason: '浏览器未实现 File System Access API。' }\n return probeCache\n }\n await navigator.storage.getDirectory()\n probeCache = { available: true }\n } catch (err) {\n probeCache = {\n available: false,\n reason: err instanceof Error ? err.message : String(err),\n }\n }\n return probeCache\n}\n\nasync function getTempDir(): Promise<FileSystemDirectoryHandle> {\n const root = await navigator.storage.getDirectory()\n return root.getDirectoryHandle(TEMP_DIR, { create: true })\n}\n\n/** 写入临时文件,返回文件名;调用方负责后续清理 */\nexport async function writeTempFile(name: string, data: Blob | ArrayBuffer): Promise<string> {\n const dir = await getTempDir()\n const handle = await dir.getFileHandle(name, { create: true })\n const writable = await handle.createWritable()\n await writable.write(data)\n await writable.close()\n return name\n}\n\n/** 读取临时文件 */\nexport async function readTempFile(name: string): Promise<File> {\n const dir = await getTempDir()\n const handle = await dir.getFileHandle(name)\n return handle.getFile()\n}\n\nexport async function removeTempFile(name: string): Promise<void> {\n try {\n const dir = await getTempDir()\n await dir.removeEntry(name)\n } catch {\n // 文件不存在时静默忽略:清理流程不应打断主流程\n }\n}\n\n/** 清空整个临时目录 */\nexport async function clearTempFiles(): Promise<void> {\n try {\n const root = await navigator.storage.getDirectory()\n await root.removeEntry(TEMP_DIR, { recursive: true })\n } catch {\n // 目录不存在时忽略\n }\n}\n\n/** 估算可用配额(用于导出前的提示) */\nexport async function estimateQuota(): Promise<{ usage: number; quota: number } | null> {\n try {\n const estimate = await navigator.storage.estimate()\n return { usage: estimate.usage ?? 0, quota: estimate.quota ?? 0 }\n } catch {\n return null\n }\n}\n"],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -17,6 +17,7 @@ export { DEFAULT_STILL_DURATION_US, DEFAULT_CLIP_VOLUME, PROJECT_SCHEMA_VERSION,
|
|
|
17
17
|
export { createId } from './lib/id';
|
|
18
18
|
export * from './timeline/time';
|
|
19
19
|
export * from './timeline/model';
|
|
20
|
+
export * from './timeline/normalize';
|
|
20
21
|
export * from './timeline/ops';
|
|
21
22
|
export * from './timeline/snap';
|
|
22
23
|
export * from './timeline/geometry';
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { i as
|
|
4
|
-
import {
|
|
1
|
+
import { a as MAX_HISTORY_STEPS, i as HISTORY_MERGE_WINDOW_MS, n as DEFAULT_CLIP_VOLUME, o as PROJECT_SCHEMA_VERSION, r as DEFAULT_STILL_DURATION_US, t as createId } from "./chunks/id-CJsdlGei.js";
|
|
2
|
+
import { a as buildTicks, c as formatTimecode, d as secToUs, f as usToMs, i as asUs, l as msToUs, m as usToSec, n as US_PER_SECOND, o as clamp, p as usToPx, r as asPx, s as formatDuration, t as US_PER_MS, u as pxToUs } from "./chunks/time-ChF1GwCy.js";
|
|
3
|
+
import { A as normalizeProject, B as getTrack, C as removeClipsData, D as DEFAULT_TEXT_STYLE, E as DEFAULT_SUBTITLE_STYLE, F as createEmptyProject, H as getTrackEnd, I as createTrack, L as getClipEnd, M as normalizeTransform, N as clampStillDuration, O as normalizeAsset, P as createClip, R as getProjectDuration, S as rangesOverlap, T as trimClipData, U as isTrackAudible, V as getTrackClips, _ as sameTransform, a as remapClipsForSize, b as duplicateClipsData, c as RESIZE_HANDLES, d as handleCursor, f as handleLocalOffset, g as resizeTransform, h as rectCenter, i as ratioLabel, j as normalizeTrack, k as normalizeClip, l as degToRad, m as pointInTransform, n as isCustomSize, o as remapTransform, p as normalizeAngle, r as normalizeSize, s as MIN_TRANSFORM_SIZE, t as ASPECT_PRESETS, u as fromLocalPoint, v as toLocalPoint, w as reorderTracksData, x as findOverlaps, y as compactTrackData, z as getSourceSpanUs } from "./chunks/canvas-size-Dlnng74w.js";
|
|
4
|
+
import { n as computeSnap, r as computeSnapWithHysteresis, t as EMPTY_SNAP } from "./chunks/snap-DpkVPVJN.js";
|
|
5
|
+
import { A as createMemoryStorage, C as isExportCancelled, D as createWebBridge, E as resolveExportSettings, M as deserializeProject, N as serializeProject, O as downloadBlob, S as ExportCancelledError, T as createExportFilename, _ as detectDependencies, a as selectAssetById, b as isEnvironmentUsable, c as selectClips, d as selectProjectDuration, f as selectSelectedClipIds, g as ENGINE_DEPENDENCIES_INSTALL_COMMAND, h as createEventBus, i as createEditorStore, j as ProjectSchemaError, k as createIndexedDbStorage, l as selectPlayheadUs, m as createHistoryStore, n as computeWaveform, o as selectAssets, p as selectTracks, r as normalizeWaveform, s as selectClipById, t as buildWaveformPath, u as selectProject, v as detectEnvironment, w as DEFAULT_EXPORT_SETTINGS, x as summarizeCapabilities, y as detectOpfs } from "./chunks/waveform-uyGNE01H.js";
|
|
5
6
|
import { n as parseSrt, r as parseSrtDurationUs, t as isValidSrt } from "./chunks/srt-D539yJ-n.js";
|
|
6
7
|
import { a as readTextFile, i as fileToStream, n as bufferToStream, r as detectFileType, t as UnsupportedFileError } from "./chunks/file-reader-OjeI7HTg.js";
|
|
7
8
|
// src/project/file-storage.ts
|
|
@@ -116,6 +117,6 @@ async function estimateQuota() {
|
|
|
116
117
|
return null;
|
|
117
118
|
}
|
|
118
119
|
}
|
|
119
|
-
export { ASPECT_PRESETS, DEFAULT_CLIP_VOLUME, DEFAULT_EXPORT_SETTINGS, DEFAULT_STILL_DURATION_US, EMPTY_SNAP, ENGINE_DEPENDENCIES_INSTALL_COMMAND, ExportCancelledError, HISTORY_MERGE_WINDOW_MS, MAX_HISTORY_STEPS, MIN_TRANSFORM_SIZE, PROJECT_SCHEMA_VERSION, ProjectSchemaError, RESIZE_HANDLES, US_PER_MS, US_PER_SECOND, UnsupportedFileError, asPx, asUs, bufferToStream, buildTicks, buildWaveformPath, clamp, clampStillDuration, clearTempFiles, compactTrackData, computeSnap, computeSnapWithHysteresis, computeWaveform, createClip, createEditorStore, createEmptyProject, createEventBus, createExportFilename, createFileStorage, createHistoryStore, createId, createIndexedDbStorage, createMemoryStorage, createTrack, createWebBridge, degToRad, deserializeProject, detectDependencies, detectEnvironment, detectFileType, detectOpfs, downloadBlob, duplicateClipsData, estimateQuota, fileToStream, findOverlaps, formatDuration, formatTimecode, fromLocalPoint, getClipEnd, getProjectDuration, getSourceSpanUs, getTrack, getTrackClips, getTrackEnd, handleCursor, handleLocalOffset, isCustomSize, isEnvironmentUsable, isExportCancelled, isTrackAudible, isValidSrt,
|
|
120
|
+
export { ASPECT_PRESETS, DEFAULT_CLIP_VOLUME, DEFAULT_EXPORT_SETTINGS, DEFAULT_STILL_DURATION_US, DEFAULT_SUBTITLE_STYLE, DEFAULT_TEXT_STYLE, EMPTY_SNAP, ENGINE_DEPENDENCIES_INSTALL_COMMAND, ExportCancelledError, HISTORY_MERGE_WINDOW_MS, MAX_HISTORY_STEPS, MIN_TRANSFORM_SIZE, PROJECT_SCHEMA_VERSION, ProjectSchemaError, RESIZE_HANDLES, US_PER_MS, US_PER_SECOND, UnsupportedFileError, asPx, asUs, bufferToStream, buildTicks, buildWaveformPath, clamp, clampStillDuration, clearTempFiles, compactTrackData, computeSnap, computeSnapWithHysteresis, computeWaveform, createClip, createEditorStore, createEmptyProject, createEventBus, createExportFilename, createFileStorage, createHistoryStore, createId, createIndexedDbStorage, createMemoryStorage, createTrack, createWebBridge, degToRad, deserializeProject, detectDependencies, detectEnvironment, detectFileType, detectOpfs, downloadBlob, duplicateClipsData, estimateQuota, fileToStream, findOverlaps, formatDuration, formatTimecode, fromLocalPoint, getClipEnd, getProjectDuration, getSourceSpanUs, getTrack, getTrackClips, getTrackEnd, handleCursor, handleLocalOffset, isCustomSize, isEnvironmentUsable, isExportCancelled, isTrackAudible, isValidSrt, msToUs, normalizeAngle, normalizeAsset, normalizeClip, normalizeProject, normalizeSize, normalizeTrack, normalizeTransform, normalizeWaveform, parseSrt, parseSrtDurationUs, pointInTransform, probeOpfs, pxToUs, rangesOverlap, ratioLabel, readTempFile, readTextFile, rectCenter, remapClipsForSize, remapTransform, removeClipsData, removeTempFile, reorderTracksData, resizeTransform, resolveExportSettings, sameTransform, secToUs, selectAssetById, selectAssets, selectClipById, selectClips, selectPlayheadUs, selectProject, selectProjectDuration, selectSelectedClipIds, selectTracks, serializeProject, summarizeCapabilities, toLocalPoint, toStorageFileName, trimClipData, usToMs, usToPx, usToSec, writeTempFile };
|
|
120
121
|
|
|
121
122
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/project/file-storage.ts","../src/media/opfs.ts"],"sourcesContent":["/**\n * 文件式存储适配器。\n *\n * 宿主要把工程存成本地 JSON(Electron 主进程写盘)时,只需要提供四个文本\n * 操作,不必理解 `SerializedProject` 的字段。\n *\n * 映射规则:`<dir>/<namespace>.json`。namespace 做一次文件系统安全化:\n * 路径分隔符、Windows 保留字符与控制字符替换为 `_`,避免宿主传入\n * `projects/p_1` 这类含层级的值时写到目录之外。\n */\n\nimport type { SerializedProject } from './schema'\nimport type { StoragePort } from './storage'\n\nexport interface FileIoAdapter {\n /** 文件不存在时返回 null(不要抛错) */\n readText(path: string): Promise<string | null>\n writeText(path: string, content: string): Promise<void>\n /** 文件不存在时静默成功 */\n remove(path: string): Promise<void>\n /** 目录不存在时返回空数组 */\n list(dir: string): Promise<string[]>\n}\n\nconst JSON_EXT = '.json'\n\n/** namespace → 安全文件名(不含扩展名) */\nexport function toStorageFileName(namespace: string): string {\n const safe = namespace\n // eslint-disable-next-line no-control-regex -- 需要把控制字符也替换掉\n .replace(/[\\\\/:*?\"<>|\\u0000-\\u001f]/g, '_')\n .replace(/^\\.+/, '_')\n .trim()\n return (safe.length > 0 ? safe : 'default') + JSON_EXT\n}\n\nfunction joinPath(dir: string, name: string): string {\n const separator = dir.includes('\\\\') && !dir.includes('/') ? '\\\\' : '/'\n return dir.endsWith('/') || dir.endsWith('\\\\') ? `${dir}${name}` : `${dir}${separator}${name}`\n}\n\nexport function createFileStorage(dir: string, io: FileIoAdapter): StoragePort {\n const pathFor = (namespace: string) => joinPath(dir, toStorageFileName(namespace))\n\n return {\n async load(namespace) {\n const text = await io.readText(pathFor(namespace))\n if (text == null || text.trim().length === 0) return null\n return JSON.parse(text) as SerializedProject\n },\n\n async save(namespace, data) {\n const path = pathFor(namespace)\n try {\n await io.writeText(path, JSON.stringify(data, null, 2))\n return { ok: true, path }\n } catch (err) {\n return { ok: false, path, error: err instanceof Error ? err.message : String(err) }\n }\n },\n\n async remove(namespace) {\n await io.remove(pathFor(namespace))\n },\n\n async list() {\n const names = await io.list(dir)\n return names\n .filter((name) => name.toLowerCase().endsWith(JSON_EXT))\n .map((name) => name.slice(0, -JSON_EXT.length))\n .sort()\n },\n }\n}\n","/**\n * OPFS 可用性探测与临时文件辅助。\n *\n * WebAV 内部(opfs-tools)在模块加载期就会访问 `navigator.storage.getDirectory()`,\n * 因此这里只使用原生 File System Access API,不 import opfs-tools,避免重复副作用。\n *\n * 使用策略:小文件直接 `file.stream()` 喂给 MP4Clip;只有需要随机读取的大文件\n * 才落盘到 OPFS 临时目录(WebAV 自身也会在内部做同样的事,这里只提供探测与清理)。\n */\n\nconst TEMP_DIR = 'cut-test-tmp'\n\nexport interface OpfsProbeResult {\n available: boolean\n reason?: string\n}\n\nlet probeCache: OpfsProbeResult | null = null\n\nexport async function probeOpfs(force = false): Promise<OpfsProbeResult> {\n if (probeCache && !force) return probeCache\n try {\n if (typeof navigator === 'undefined' || navigator.storage?.getDirectory == null) {\n probeCache = { available: false, reason: '浏览器未实现 File System Access API。' }\n return probeCache\n }\n await navigator.storage.getDirectory()\n probeCache = { available: true }\n } catch (err) {\n probeCache = {\n available: false,\n reason: err instanceof Error ? err.message : String(err),\n }\n }\n return probeCache\n}\n\nasync function getTempDir(): Promise<FileSystemDirectoryHandle> {\n const root = await navigator.storage.getDirectory()\n return root.getDirectoryHandle(TEMP_DIR, { create: true })\n}\n\n/** 写入临时文件,返回文件名;调用方负责后续清理 */\nexport async function writeTempFile(name: string, data: Blob | ArrayBuffer): Promise<string> {\n const dir = await getTempDir()\n const handle = await dir.getFileHandle(name, { create: true })\n const writable = await handle.createWritable()\n await writable.write(data)\n await writable.close()\n return name\n}\n\n/** 读取临时文件 */\nexport async function readTempFile(name: string): Promise<File> {\n const dir = await getTempDir()\n const handle = await dir.getFileHandle(name)\n return handle.getFile()\n}\n\nexport async function removeTempFile(name: string): Promise<void> {\n try {\n const dir = await getTempDir()\n await dir.removeEntry(name)\n } catch {\n // 文件不存在时静默忽略:清理流程不应打断主流程\n }\n}\n\n/** 清空整个临时目录 */\nexport async function clearTempFiles(): Promise<void> {\n try {\n const root = await navigator.storage.getDirectory()\n await root.removeEntry(TEMP_DIR, { recursive: true })\n } catch {\n // 目录不存在时忽略\n }\n}\n\n/** 估算可用配额(用于导出前的提示) */\nexport async function estimateQuota(): Promise<{ usage: number; quota: number } | null> {\n try {\n const estimate = await navigator.storage.estimate()\n return { usage: estimate.usage ?? 0, quota: estimate.quota ?? 0 }\n } catch {\n return null\n }\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/project/file-storage.ts","../src/media/opfs.ts"],"sourcesContent":["/**\n * 文件式存储适配器。\n *\n * 宿主要把工程存成本地 JSON(Electron 主进程写盘)时,只需要提供四个文本\n * 操作,不必理解 `SerializedProject` 的字段。\n *\n * 映射规则:`<dir>/<namespace>.json`。namespace 做一次文件系统安全化:\n * 路径分隔符、Windows 保留字符与控制字符替换为 `_`,避免宿主传入\n * `projects/p_1` 这类含层级的值时写到目录之外。\n */\n\nimport type { SerializedProject } from './schema'\nimport type { StoragePort } from './storage'\n\nexport interface FileIoAdapter {\n /** 文件不存在时返回 null(不要抛错) */\n readText(path: string): Promise<string | null>\n writeText(path: string, content: string): Promise<void>\n /** 文件不存在时静默成功 */\n remove(path: string): Promise<void>\n /** 目录不存在时返回空数组 */\n list(dir: string): Promise<string[]>\n}\n\nconst JSON_EXT = '.json'\n\n/** namespace → 安全文件名(不含扩展名) */\nexport function toStorageFileName(namespace: string): string {\n const safe = namespace\n // eslint-disable-next-line no-control-regex -- 需要把控制字符也替换掉\n .replace(/[\\\\/:*?\"<>|\\u0000-\\u001f]/g, '_')\n .replace(/^\\.+/, '_')\n .trim()\n return (safe.length > 0 ? safe : 'default') + JSON_EXT\n}\n\nfunction joinPath(dir: string, name: string): string {\n const separator = dir.includes('\\\\') && !dir.includes('/') ? '\\\\' : '/'\n return dir.endsWith('/') || dir.endsWith('\\\\') ? `${dir}${name}` : `${dir}${separator}${name}`\n}\n\nexport function createFileStorage(dir: string, io: FileIoAdapter): StoragePort {\n const pathFor = (namespace: string) => joinPath(dir, toStorageFileName(namespace))\n\n return {\n async load(namespace) {\n const text = await io.readText(pathFor(namespace))\n if (text == null || text.trim().length === 0) return null\n return JSON.parse(text) as SerializedProject\n },\n\n async save(namespace, data) {\n const path = pathFor(namespace)\n try {\n await io.writeText(path, JSON.stringify(data, null, 2))\n return { ok: true, path }\n } catch (err) {\n return { ok: false, path, error: err instanceof Error ? err.message : String(err) }\n }\n },\n\n async remove(namespace) {\n await io.remove(pathFor(namespace))\n },\n\n async list() {\n const names = await io.list(dir)\n return names\n .filter((name) => name.toLowerCase().endsWith(JSON_EXT))\n .map((name) => name.slice(0, -JSON_EXT.length))\n .sort()\n },\n }\n}\n","/**\n * OPFS 可用性探测与临时文件辅助。\n *\n * WebAV 内部(opfs-tools)在模块加载期就会访问 `navigator.storage.getDirectory()`,\n * 因此这里只使用原生 File System Access API,不 import opfs-tools,避免重复副作用。\n *\n * 使用策略:小文件直接 `file.stream()` 喂给 MP4Clip;只有需要随机读取的大文件\n * 才落盘到 OPFS 临时目录(WebAV 自身也会在内部做同样的事,这里只提供探测与清理)。\n */\n\nconst TEMP_DIR = 'cut-test-tmp'\n\nexport interface OpfsProbeResult {\n available: boolean\n reason?: string\n}\n\nlet probeCache: OpfsProbeResult | null = null\n\nexport async function probeOpfs(force = false): Promise<OpfsProbeResult> {\n if (probeCache && !force) return probeCache\n try {\n if (typeof navigator === 'undefined' || navigator.storage?.getDirectory == null) {\n probeCache = { available: false, reason: '浏览器未实现 File System Access API。' }\n return probeCache\n }\n await navigator.storage.getDirectory()\n probeCache = { available: true }\n } catch (err) {\n probeCache = {\n available: false,\n reason: err instanceof Error ? err.message : String(err),\n }\n }\n return probeCache\n}\n\nasync function getTempDir(): Promise<FileSystemDirectoryHandle> {\n const root = await navigator.storage.getDirectory()\n return root.getDirectoryHandle(TEMP_DIR, { create: true })\n}\n\n/** 写入临时文件,返回文件名;调用方负责后续清理 */\nexport async function writeTempFile(name: string, data: Blob | ArrayBuffer): Promise<string> {\n const dir = await getTempDir()\n const handle = await dir.getFileHandle(name, { create: true })\n const writable = await handle.createWritable()\n await writable.write(data)\n await writable.close()\n return name\n}\n\n/** 读取临时文件 */\nexport async function readTempFile(name: string): Promise<File> {\n const dir = await getTempDir()\n const handle = await dir.getFileHandle(name)\n return handle.getFile()\n}\n\nexport async function removeTempFile(name: string): Promise<void> {\n try {\n const dir = await getTempDir()\n await dir.removeEntry(name)\n } catch {\n // 文件不存在时静默忽略:清理流程不应打断主流程\n }\n}\n\n/** 清空整个临时目录 */\nexport async function clearTempFiles(): Promise<void> {\n try {\n const root = await navigator.storage.getDirectory()\n await root.removeEntry(TEMP_DIR, { recursive: true })\n } catch {\n // 目录不存在时忽略\n }\n}\n\n/** 估算可用配额(用于导出前的提示) */\nexport async function estimateQuota(): Promise<{ usage: number; quota: number } | null> {\n try {\n const estimate = await navigator.storage.estimate()\n return { usage: estimate.usage ?? 0, quota: estimate.quota ?? 0 }\n } catch {\n return null\n }\n}\n"],"mappings":"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"}
|
|
@@ -30,6 +30,14 @@ export interface ThumbnailSource {
|
|
|
30
30
|
* @param width 单张宽度
|
|
31
31
|
*/
|
|
32
32
|
export declare function generateThumbnails(clip: ThumbnailSource, assetId: string, count: number, durationUs: number, width?: number): Promise<ThumbnailFrame[]>;
|
|
33
|
+
/**
|
|
34
|
+
* 绕过缓存重新抽帧。
|
|
35
|
+
*
|
|
36
|
+
* 重新打开工程后素材 id 不变,但旧的 `blob:` URL 已经失效;缓存 key 是 assetId,
|
|
37
|
+
* 直接走 `generateThumbnails` 会命中失效缓存(卡片显示破图/占位)。
|
|
38
|
+
* 这里先释放旧缓存再抽一次。
|
|
39
|
+
*/
|
|
40
|
+
export declare function regenerateThumbnails(clip: ThumbnailSource, assetId: string, count: number, durationUs: number, width?: number): Promise<ThumbnailFrame[]>;
|
|
33
41
|
/** 释放某素材的缩略图 URL */
|
|
34
42
|
export declare function releaseThumbnails(assetId: string): void;
|
|
35
43
|
/** 抽帧结果转成时间戳列表(Lab 使用) */
|
package/dist/project/schema.d.ts
CHANGED
|
@@ -3,16 +3,21 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 版本号用于将来迁移。当前 schema v1:Project 原样存储,素材元数据单独存。
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* 关于素材二进制:`SerializedProject` **不含** File。落盘的是
|
|
7
|
+
* 「素材元数据 + 宿主可解析的引用」,二进制由宿主负责:
|
|
8
|
+
* - 想恢复成片,宿主用 `importJson(data, { resolveFile })` 按 `asset.id` 找回本地文件;
|
|
9
|
+
* - 或在保存前把文件写进 OPFS / 本地目录,再把路径放进 `asset.sourcePath`。
|
|
10
|
+
*
|
|
11
|
+
* 为什么不做「把 File 塞进 IndexedDB」:Blob 可以存,但几十上百 MB 的素材会让
|
|
12
|
+
* 每次自动保存都复制一遍二进制,且跨浏览器配额差异极大;更关键的是宿主往往
|
|
13
|
+
* 已经有自己的素材库,重复存一份反而失去「工程 ↔ 宿主记录」的对应关系。
|
|
9
14
|
*/
|
|
10
15
|
import type { MediaAsset, Project } from '../types';
|
|
11
16
|
export interface SerializedProject {
|
|
12
17
|
schemaVersion: number;
|
|
13
18
|
savedAt: number;
|
|
14
19
|
project: Project;
|
|
15
|
-
/**
|
|
20
|
+
/** 素材元数据(不含二进制);binary 缺失时仅能恢复占位卡片 */
|
|
16
21
|
assets: MediaAsset[];
|
|
17
22
|
}
|
|
18
23
|
export interface LoadResult {
|
|
@@ -21,6 +26,13 @@ export interface LoadResult {
|
|
|
21
26
|
/** 缺少二进制源的素材 id(UI 用占位显示) */
|
|
22
27
|
missingAssetIds: string[];
|
|
23
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* 生成可落盘的工程快照。
|
|
31
|
+
*
|
|
32
|
+
* ⚠️ 必须是**深拷贝**:返回值会交给宿主(写入本地 JSON / 走 IPC)。
|
|
33
|
+
* 若直接返回 store 里的对象引用,宿主对这些字段的任何改动都会反向污染编辑器状态,
|
|
34
|
+
* 异步写盘期间发生编辑也会让「存下去的内容」与「保存时刻的状态」不一致。
|
|
35
|
+
*/
|
|
24
36
|
export declare function serializeProject(project: Project, assets: MediaAsset[]): SerializedProject;
|
|
25
37
|
export declare class ProjectSchemaError extends Error {
|
|
26
38
|
constructor(message: string);
|
package/dist/timeline/index.cjs
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_time = require("../chunks/time-
|
|
3
|
-
const require_canvas_size = require("../chunks/canvas-size-
|
|
4
|
-
const require_snap = require("../chunks/snap-
|
|
2
|
+
const require_time = require("../chunks/time-CYEd91qN.cjs");
|
|
3
|
+
const require_canvas_size = require("../chunks/canvas-size-DIABzEW2.cjs");
|
|
4
|
+
const require_snap = require("../chunks/snap-Cfk15JVg.cjs");
|
|
5
5
|
exports.ASPECT_PRESETS = require_canvas_size.ASPECT_PRESETS;
|
|
6
|
+
exports.DEFAULT_SUBTITLE_STYLE = require_canvas_size.DEFAULT_SUBTITLE_STYLE;
|
|
7
|
+
exports.DEFAULT_TEXT_STYLE = require_canvas_size.DEFAULT_TEXT_STYLE;
|
|
6
8
|
exports.EMPTY_SNAP = require_snap.EMPTY_SNAP;
|
|
7
9
|
exports.MIN_TRANSFORM_SIZE = require_canvas_size.MIN_TRANSFORM_SIZE;
|
|
8
10
|
exports.RESIZE_HANDLES = require_canvas_size.RESIZE_HANDLES;
|
|
@@ -35,11 +37,14 @@ exports.handleCursor = require_canvas_size.handleCursor;
|
|
|
35
37
|
exports.handleLocalOffset = require_canvas_size.handleLocalOffset;
|
|
36
38
|
exports.isCustomSize = require_canvas_size.isCustomSize;
|
|
37
39
|
exports.isTrackAudible = require_canvas_size.isTrackAudible;
|
|
38
|
-
exports.moveClipData = require_canvas_size.moveClipData;
|
|
39
40
|
exports.msToUs = require_time.msToUs;
|
|
40
|
-
exports.nearestSorted = require_snap.nearestSorted;
|
|
41
41
|
exports.normalizeAngle = require_canvas_size.normalizeAngle;
|
|
42
|
+
exports.normalizeAsset = require_canvas_size.normalizeAsset;
|
|
43
|
+
exports.normalizeClip = require_canvas_size.normalizeClip;
|
|
44
|
+
exports.normalizeProject = require_canvas_size.normalizeProject;
|
|
42
45
|
exports.normalizeSize = require_canvas_size.normalizeSize;
|
|
46
|
+
exports.normalizeTrack = require_canvas_size.normalizeTrack;
|
|
47
|
+
exports.normalizeTransform = require_canvas_size.normalizeTransform;
|
|
43
48
|
exports.pointInTransform = require_canvas_size.pointInTransform;
|
|
44
49
|
exports.pxToUs = require_time.pxToUs;
|
|
45
50
|
exports.rangesOverlap = require_canvas_size.rangesOverlap;
|
|
@@ -52,7 +57,6 @@ exports.reorderTracksData = require_canvas_size.reorderTracksData;
|
|
|
52
57
|
exports.resizeTransform = require_canvas_size.resizeTransform;
|
|
53
58
|
exports.sameTransform = require_canvas_size.sameTransform;
|
|
54
59
|
exports.secToUs = require_time.secToUs;
|
|
55
|
-
exports.splitClipData = require_canvas_size.splitClipData;
|
|
56
60
|
exports.toLocalPoint = require_canvas_size.toLocalPoint;
|
|
57
61
|
exports.trimClipData = require_canvas_size.trimClipData;
|
|
58
62
|
exports.usToMs = require_time.usToMs;
|
package/dist/timeline/index.d.ts
CHANGED
package/dist/timeline/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as buildTicks, c as formatTimecode, d as secToUs, f as usToMs, i as asUs, l as msToUs, m as usToSec, n as US_PER_SECOND, o as clamp, p as usToPx, r as asPx, s as formatDuration, t as US_PER_MS, u as pxToUs } from "../chunks/time-
|
|
2
|
-
import { A as
|
|
3
|
-
import {
|
|
4
|
-
export { ASPECT_PRESETS, EMPTY_SNAP, MIN_TRANSFORM_SIZE, RESIZE_HANDLES, US_PER_MS, US_PER_SECOND, asPx, asUs, buildTicks, clamp, clampStillDuration, compactTrackData, computeSnap, computeSnapWithHysteresis, createClip, createEmptyProject, createTrack, degToRad, duplicateClipsData, findOverlaps, formatDuration, formatTimecode, fromLocalPoint, getClipEnd, getProjectDuration, getSourceSpanUs, getTrack, getTrackClips, getTrackEnd, handleCursor, handleLocalOffset, isCustomSize, isTrackAudible,
|
|
1
|
+
import { a as buildTicks, c as formatTimecode, d as secToUs, f as usToMs, i as asUs, l as msToUs, m as usToSec, n as US_PER_SECOND, o as clamp, p as usToPx, r as asPx, s as formatDuration, t as US_PER_MS, u as pxToUs } from "../chunks/time-ChF1GwCy.js";
|
|
2
|
+
import { A as normalizeProject, B as getTrack, C as removeClipsData, D as DEFAULT_TEXT_STYLE, E as DEFAULT_SUBTITLE_STYLE, F as createEmptyProject, H as getTrackEnd, I as createTrack, L as getClipEnd, M as normalizeTransform, N as clampStillDuration, O as normalizeAsset, P as createClip, R as getProjectDuration, S as rangesOverlap, T as trimClipData, U as isTrackAudible, V as getTrackClips, _ as sameTransform, a as remapClipsForSize, b as duplicateClipsData, c as RESIZE_HANDLES, d as handleCursor, f as handleLocalOffset, g as resizeTransform, h as rectCenter, i as ratioLabel, j as normalizeTrack, k as normalizeClip, l as degToRad, m as pointInTransform, n as isCustomSize, o as remapTransform, p as normalizeAngle, r as normalizeSize, s as MIN_TRANSFORM_SIZE, t as ASPECT_PRESETS, u as fromLocalPoint, v as toLocalPoint, w as reorderTracksData, x as findOverlaps, y as compactTrackData, z as getSourceSpanUs } from "../chunks/canvas-size-Dlnng74w.js";
|
|
3
|
+
import { n as computeSnap, r as computeSnapWithHysteresis, t as EMPTY_SNAP } from "../chunks/snap-DpkVPVJN.js";
|
|
4
|
+
export { ASPECT_PRESETS, DEFAULT_SUBTITLE_STYLE, DEFAULT_TEXT_STYLE, EMPTY_SNAP, MIN_TRANSFORM_SIZE, RESIZE_HANDLES, US_PER_MS, US_PER_SECOND, asPx, asUs, buildTicks, clamp, clampStillDuration, compactTrackData, computeSnap, computeSnapWithHysteresis, createClip, createEmptyProject, createTrack, degToRad, duplicateClipsData, findOverlaps, formatDuration, formatTimecode, fromLocalPoint, getClipEnd, getProjectDuration, getSourceSpanUs, getTrack, getTrackClips, getTrackEnd, handleCursor, handleLocalOffset, isCustomSize, isTrackAudible, msToUs, normalizeAngle, normalizeAsset, normalizeClip, normalizeProject, normalizeSize, normalizeTrack, normalizeTransform, pointInTransform, pxToUs, rangesOverlap, ratioLabel, rectCenter, remapClipsForSize, remapTransform, removeClipsData, reorderTracksData, resizeTransform, sameTransform, secToUs, toLocalPoint, trimClipData, usToMs, usToPx, usToSec };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 工程数据的容错归一化。
|
|
3
|
+
*
|
|
4
|
+
* 为什么需要:工程 JSON 是**跨版本、跨来源**的长期文件 ——
|
|
5
|
+
* - 宿主可能手工拼、用脚本生成、或在旧版本里存的(那时还没有 `aspectLocked`
|
|
6
|
+
* 这类后加字段);
|
|
7
|
+
* - 我们自己也持续加字段。
|
|
8
|
+
*
|
|
9
|
+
* 直接把外部 JSON 塞进 store 会让 UI 在 `clip.transform.x` 这类读取上抛
|
|
10
|
+
* "Cannot read properties of undefined",而这种错误发生在渲染期,用户只能
|
|
11
|
+
* 看到白屏。这里把所有可选字段补成默认值,并把明显损坏的数值收敛到合法范围,
|
|
12
|
+
* 让「字段缺失」降级为「使用默认值」。
|
|
13
|
+
*
|
|
14
|
+
* 本文件是纯函数,不依赖 WebAV / React。
|
|
15
|
+
*/
|
|
16
|
+
import type { ClipTransform, MediaAsset, Project, SubtitleStyle, TextStyle, TimelineClip, Track } from '../types';
|
|
17
|
+
declare const DEFAULT_TEXT_STYLE: TextStyle;
|
|
18
|
+
declare const DEFAULT_SUBTITLE_STYLE: SubtitleStyle;
|
|
19
|
+
/** 补全 clip 的几何变换(缺 transform / 缺子字段都会兜底) */
|
|
20
|
+
export declare function normalizeTransform(raw: unknown, projectWidth: number, projectHeight: number): ClipTransform;
|
|
21
|
+
/** 补全单个 clip;`id` 缺失时生成一个,保证后续操作可用 */
|
|
22
|
+
export declare function normalizeClip(raw: unknown, projectWidth: number, projectHeight: number): TimelineClip | null;
|
|
23
|
+
export declare function normalizeTrack(raw: unknown, index: number): Track | null;
|
|
24
|
+
/** 补全工程;轨道为空时补一对默认轨道,否则 clip 无处可放 */
|
|
25
|
+
export declare function normalizeProject(raw: unknown): Project | null;
|
|
26
|
+
/** 补全素材元数据;`kind` 非法的条目直接丢弃(无法决定如何渲染) */
|
|
27
|
+
export declare function normalizeAsset(raw: unknown): MediaAsset | null;
|
|
28
|
+
export { DEFAULT_TEXT_STYLE, DEFAULT_SUBTITLE_STYLE };
|
package/dist/timeline/ops.d.ts
CHANGED
|
@@ -5,10 +5,6 @@
|
|
|
5
5
|
* `core/webav/clip-registry.ts` 负责,模型层只处理时间与轨道数据。
|
|
6
6
|
*/
|
|
7
7
|
import type { TimelineClip, Track } from '../types';
|
|
8
|
-
/** 在时间轴时刻 `atUs` 分割 clip,返回 [左, 右];时刻不在 clip 内部时返回 null */
|
|
9
|
-
export declare function splitClipData(clip: TimelineClip, atUs: number): [TimelineClip, TimelineClip] | null;
|
|
10
|
-
/** 移动 clip:更新轨道与起点(不允许早于 0) */
|
|
11
|
-
export declare function moveClipData(clip: TimelineClip, trackId: string, startUs: number): TimelineClip;
|
|
12
8
|
export type TrimEdge = 'start' | 'end';
|
|
13
9
|
/**
|
|
14
10
|
* 裁剪 clip 的边缘。
|
package/dist/timeline/snap.d.ts
CHANGED
|
@@ -87,5 +87,3 @@ export declare function computeSnap(input: SnapInput): SnapResult;
|
|
|
87
87
|
* 否则这个分支永远不可能生效(旧实现就是如此)。
|
|
88
88
|
*/
|
|
89
89
|
export declare function computeSnapWithHysteresis(input: SnapInput, releaseThresholdPx?: number): SnapResult;
|
|
90
|
-
/** 二分查找:在已排序的时间数组中找最接近的值 */
|
|
91
|
-
export declare function nearestSorted(sortedUs: number[], targetUs: number): number | null;
|
package/dist/types.d.ts
CHANGED
|
@@ -44,10 +44,22 @@ export interface MediaAsset {
|
|
|
44
44
|
height: number;
|
|
45
45
|
hasAudio: boolean;
|
|
46
46
|
createdAt: number;
|
|
47
|
-
/** 视频抽帧缩略图(object URL
|
|
47
|
+
/** 视频抽帧缩略图(object URL,运行时生成,**不持久化**) */
|
|
48
48
|
thumbnailUrls?: string[];
|
|
49
|
-
/**
|
|
49
|
+
/**
|
|
50
|
+
* 音频波形峰值(0~1)。
|
|
51
|
+
*
|
|
52
|
+
* 纯派生数据:不写入工程 JSON(400 个浮点数占 ~4KB,且恢复后能重算)。
|
|
53
|
+
* 会话内由导入流水线填充,UI 波形图直接读它。
|
|
54
|
+
*/
|
|
50
55
|
waveform?: number[];
|
|
56
|
+
/**
|
|
57
|
+
* 宿主侧的源文件引用(路径 / 素材库 id / URL),由宿主写入、库原样透传。
|
|
58
|
+
*
|
|
59
|
+
* 重新打开工程时交给 `importJson({ resolveFile })` 找回二进制;
|
|
60
|
+
* 库不解释它的格式。
|
|
61
|
+
*/
|
|
62
|
+
sourcePath?: string;
|
|
51
63
|
/** 文字素材内容与样式 */
|
|
52
64
|
text?: string;
|
|
53
65
|
textStyle?: TextStyle;
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"canvas-size-CM2oKApi.js","names":[],"sources":["../../src/timeline/model.ts","../../src/timeline/ops.ts","../../src/timeline/geometry.ts","../../src/timeline/canvas-size.ts"],"sourcesContent":["/**\n * 时间轴纯数据模型与工厂函数。\n *\n * 这里没有任何 WebAV / React 依赖,所有函数都是纯函数或纯构造。\n * 时间单位统一微秒(µs)。\n */\n\nimport { createId } from '../lib/id'\nimport { DEFAULT_CLIP_VOLUME, DEFAULT_STILL_DURATION_US } from '../constants'\nimport type { Project, TimelineClip, Track } from '../types'\nimport { asUs } from './time'\n\nexport function createTrack(kind: Track['kind'], name: string): Track {\n return {\n id: createId('track'),\n name,\n kind,\n locked: false,\n muted: false,\n hidden: false,\n }\n}\n\nexport function createEmptyProject(name = '未命名工程'): Project {\n return {\n id: createId('project'),\n name,\n width: 1280,\n height: 720,\n tracks: [createTrack('visual', '视频 1'), createTrack('audio', '音频 1')],\n clips: [],\n updatedAt: Date.now(),\n }\n}\n\nexport interface CreateClipInput {\n assetId: string\n trackId: string\n startUs: number\n durationUs: number\n inPointUs?: number\n assetWidth?: number\n assetHeight?: number\n}\n\n/** 新建 clip 的默认几何:等比铺满工程画面 */\nexport function createClip(input: CreateClipInput, project: Project): TimelineClip {\n const { assetWidth = 0, assetHeight = 0 } = input\n let w = project.width\n let h = project.height\n if (assetWidth > 0 && assetHeight > 0) {\n const scale = Math.min(project.width / assetWidth, project.height / assetHeight)\n w = Math.round(assetWidth * scale)\n h = Math.round(assetHeight * scale)\n }\n return {\n id: createId('clip'),\n trackId: input.trackId,\n assetId: input.assetId,\n startUs: asUs(input.startUs),\n durationUs: asUs(input.durationUs),\n inPointUs: asUs(input.inPointUs ?? 0),\n playbackRate: 1,\n volume: DEFAULT_CLIP_VOLUME,\n muted: false,\n opacity: 1,\n flip: null,\n transform: {\n x: Math.round((project.width - w) / 2),\n y: Math.round((project.height - h) / 2),\n w,\n h,\n angle: 0,\n },\n fadeInUs: asUs(0),\n fadeOutUs: asUs(0),\n }\n}\n\n/** 静态素材(图片 / 文字 / 字幕)需要有限时长 */\nexport function clampStillDuration(us: number): number {\n return us > 0 && Number.isFinite(us) ? us : DEFAULT_STILL_DURATION_US\n}\n\nexport function getClipEnd(clip: TimelineClip): number {\n return clip.startUs + clip.durationUs\n}\n\n/** 工程总时长(取所有 clip 结束时间的最大值) */\nexport function getProjectDuration(project: Project): number {\n return project.clips.reduce((max, c) => Math.max(max, getClipEnd(c)), 0)\n}\n\nexport function getTrack(project: Project, trackId: string): Track | undefined {\n return project.tracks.find((t) => t.id === trackId)\n}\n\nexport function isTrackAudible(project: Project, trackId: string): boolean {\n const track = getTrack(project, trackId)\n if (!track) return false\n return !track.muted && !track.hidden\n}\n\n/** 某轨道上按时间排序的 clip */\nexport function getTrackClips(project: Project, trackId: string): TimelineClip[] {\n return project.clips\n .filter((c) => c.trackId === trackId)\n .sort((a, b) => a.startUs - b.startUs)\n}\n\n/** 轨道内最大结束时间,用于「轨道末尾」吸附点 */\nexport function getTrackEnd(project: Project, trackId: string): number {\n return getTrackClips(project, trackId).reduce((max, c) => Math.max(max, getClipEnd(c)), 0)\n}\n\n/** 源素材被消费的时长 = 时间轴时长 × 速率 */\nexport function getSourceSpanUs(clip: TimelineClip): number {\n return Math.round(clip.durationUs * clip.playbackRate)\n}\n","/**\n * 时间轴编辑操作(纯函数)。\n *\n * 全部返回新的数组/对象,不修改入参;异步部分(如真正切割 IClip 实例)由\n * `core/webav/clip-registry.ts` 负责,模型层只处理时间与轨道数据。\n */\n\nimport type { TimelineClip, Track } from '../types'\nimport { asUs } from './time'\nimport { getClipEnd } from './model'\n\n/** 在时间轴时刻 `atUs` 分割 clip,返回 [左, 右];时刻不在 clip 内部时返回 null */\nexport function splitClipData(clip: TimelineClip, atUs: number): [TimelineClip, TimelineClip] | null {\n const start = clip.startUs\n const end = getClipEnd(clip)\n if (atUs <= start || atUs >= end) return null\n const leftDuration = atUs - start\n const rightDuration = end - atUs\n const sourceOffset = Math.round(leftDuration * clip.playbackRate)\n\n const left: TimelineClip = {\n ...clip,\n durationUs: asUs(leftDuration),\n fadeOutUs: asUs(Math.min(clip.fadeOutUs, leftDuration)),\n }\n const right: TimelineClip = {\n ...clip,\n id: `${clip.id}_r`,\n startUs: asUs(atUs),\n durationUs: asUs(rightDuration),\n inPointUs: asUs(clip.inPointUs + sourceOffset),\n fadeInUs: asUs(Math.min(clip.fadeInUs, rightDuration)),\n }\n left.fadeInUs = asUs(Math.min(clip.fadeInUs, leftDuration))\n return [left, right]\n}\n\n/** 移动 clip:更新轨道与起点(不允许早于 0) */\nexport function moveClipData(clip: TimelineClip, trackId: string, startUs: number): TimelineClip {\n return { ...clip, trackId, startUs: asUs(Math.max(0, startUs)) }\n}\n\nexport type TrimEdge = 'start' | 'end'\n\n/**\n * 裁剪 clip 的边缘。\n *\n * - start:改变起点与源入点,保持右边缘不动\n * - end:只改变时长\n *\n * `minDurationUs` 保证不会裁成 0 长度。\n */\nexport function trimClipData(\n clip: TimelineClip,\n edge: TrimEdge,\n deltaUs: number,\n minDurationUs = 100_000,\n): TimelineClip {\n const end = getClipEnd(clip)\n\n if (edge === 'start') {\n const nextStart = Math.min(\n Math.max(0, clip.startUs + deltaUs),\n end - minDurationUs,\n )\n const applied = nextStart - clip.startUs\n const sourceDelta = Math.round(applied * clip.playbackRate)\n const nextInPoint = Math.max(0, clip.inPointUs + sourceDelta)\n return {\n ...clip,\n startUs: asUs(nextStart),\n inPointUs: asUs(nextInPoint),\n durationUs: asUs(end - nextStart),\n }\n }\n\n const nextDuration = Math.max(minDurationUs, clip.durationUs + deltaUs)\n return { ...clip, durationUs: asUs(nextDuration) }\n}\n\n/** 删除指定 id 的 clip */\nexport function removeClipsData(clips: TimelineClip[], ids: string[]): TimelineClip[] {\n const set = new Set(ids)\n return clips.filter((c) => !set.has(c.id))\n}\n\n/** 复制 clip,起点顺延 `offsetUs`,返回 { originalId, copy } 便于重建 IClip 实例 */\nexport function duplicateClipsData(\n clips: TimelineClip[],\n ids: string[],\n offsetUs: number,\n makeId: () => string,\n): Array<{ sourceId: string; clip: TimelineClip }> {\n const set = new Set(ids)\n return clips\n .filter((c) => set.has(c.id))\n .map((c) => ({\n sourceId: c.id,\n clip: {\n ...c,\n id: makeId(),\n startUs: asUs(c.startUs + offsetUs),\n transform: { ...c.transform },\n },\n }))\n}\n\n/** 自动排布:把同一轨道上的 clip 按顺序首尾相接(用于「磁吸排序」) */\nexport function compactTrackData(clips: TimelineClip[], trackId: string): TimelineClip[] {\n const onTrack = clips\n .filter((c) => c.trackId === trackId)\n .sort((a, b) => a.startUs - b.startUs)\n let cursor = 0\n const updated = new Map<string, number>()\n for (const c of onTrack) {\n updated.set(c.id, cursor)\n cursor += c.durationUs\n }\n return clips.map((c) => {\n const next = updated.get(c.id)\n return next == null ? c : { ...c, startUs: asUs(next) }\n })\n}\n\n/** 重新排序轨道(拖拽调整垂直顺序) */\nexport function reorderTracksData(tracks: Track[], trackId: string, toIndex: number): Track[] {\n const from = tracks.findIndex((t) => t.id === trackId)\n if (from < 0) return tracks\n const next = [...tracks]\n const [moved] = next.splice(from, 1)\n if (!moved) return tracks\n next.splice(Math.max(0, Math.min(next.length, toIndex)), 0, moved)\n return next\n}\n\n/** 判断两个区间是否重叠(半开区间) */\nexport function rangesOverlap(aStart: number, aEnd: number, bStart: number, bEnd: number): boolean {\n return aStart < bEnd && bStart < aEnd\n}\n\n/** 找出与给定区间在指定轨道上重叠的 clip(排除自身) */\nexport function findOverlaps(\n clips: TimelineClip[],\n trackId: string,\n startUs: number,\n endUs: number,\n excludeId?: string,\n): TimelineClip[] {\n return clips.filter(\n (c) =>\n c.trackId === trackId &&\n c.id !== excludeId &&\n rangesOverlap(startUs, endUs, c.startUs, getClipEnd(c)),\n )\n}\n","/**\n * 画布几何计算(纯函数)。\n *\n * 坐标系:工程分辨率像素(`project.width × project.height`),\n * 与 `clip.transform` 完全一致,也与导出坐标映射的输入一致。\n *\n * 为什么单独放 core:画布的命中测试、自绘选框的手柄布局、缩放反算\n * 这三处必须用同一套数学,否则「点中的框」和「画出的框」会错位。\n */\n\nimport type { ClipTransform } from '../types'\n\n/** 手柄方位。边用 4 个字母,角用两两组合,另加旋转 */\nexport type HandleKey = 'n' | 's' | 'e' | 'w' | 'ne' | 'nw' | 'se' | 'sw'\n\nexport const RESIZE_HANDLES: HandleKey[] = ['nw', 'n', 'ne', 'e', 'se', 's', 'sw', 'w']\n\n/** 最小尺寸(工程像素),与 WebAV 的内部控制点下限保持一致 */\nexport const MIN_TRANSFORM_SIZE = 10\n\nexport interface Point {\n x: number\n y: number\n}\n\nexport interface Rect {\n x: number\n y: number\n w: number\n h: number\n}\n\nexport function rectCenter(rect: Rect): Point {\n return { x: rect.x + rect.w / 2, y: rect.y + rect.h / 2 }\n}\n\n/** 角度(度)→ 弧度 */\nexport function degToRad(deg: number): number {\n return (deg * Math.PI) / 180\n}\n\n/**\n * 把世界坐标点转换到矩形自身的局部坐标(以中心为原点、已抵消旋转)。\n *\n * 旋转方向与 Canvas `rotate()` 一致(顺时针为正),因此用 `-angle` 做逆变换。\n */\nexport function toLocalPoint(point: Point, transform: Pick<ClipTransform, 'x' | 'y' | 'w' | 'h' | 'angle'>): Point {\n const cx = transform.x + transform.w / 2\n const cy = transform.y + transform.h / 2\n const rad = -degToRad(transform.angle)\n const dx = point.x - cx\n const dy = point.y - cy\n return {\n x: dx * Math.cos(rad) - dy * Math.sin(rad),\n y: dx * Math.sin(rad) + dy * Math.cos(rad),\n }\n}\n\n/** 局部坐标(相对中心)→ 世界坐标 */\nexport function fromLocalPoint(local: Point, transform: Pick<ClipTransform, 'x' | 'y' | 'w' | 'h' | 'angle'>): Point {\n const cx = transform.x + transform.w / 2\n const cy = transform.y + transform.h / 2\n const rad = degToRad(transform.angle)\n return {\n x: cx + local.x * Math.cos(rad) - local.y * Math.sin(rad),\n y: cy + local.x * Math.sin(rad) + local.y * Math.cos(rad),\n }\n}\n\n/** 点是否落在(可能旋转的)矩形内 */\nexport function pointInTransform(point: Point, transform: ClipTransform): boolean {\n if (transform.w <= 0 || transform.h <= 0) return false\n const local = toLocalPoint(point, transform)\n return Math.abs(local.x) <= transform.w / 2 && Math.abs(local.y) <= transform.h / 2\n}\n\n/**\n * 按方位返回手柄在**局部坐标**中的位置(相对中心)。\n *\n * 边手柄固定一个方向为 0,角手柄两个方向都取半宽/半高。\n */\nexport function handleLocalOffset(handle: HandleKey, w: number, h: number): Point {\n const hw = w / 2\n const hh = h / 2\n const x = handle.includes('w') ? -hw : handle.includes('e') ? hw : 0\n const y = handle.includes('n') ? -hh : handle.includes('s') ? hh : 0\n return { x, y }\n}\n\n/**\n * 计算缩放后的 transform。\n *\n * 采用「固定对角/对边」的直觉行为:拖右边缘时左边缘不动,拖左上角时右下角不动。\n * 指针先被投影到局部坐标轴,因此素材旋转后手柄仍沿自身方向缩放(而不是屏幕方向)。\n *\n * @param start 拖动开始时的 transform 快照\n * @param handle 被拖动的方位\n * @param pointer 当前指针(工程坐标)\n * @param keepAspect\n * - `false`(默认):自由缩放\n * - `true`:所有手柄都等比(Shift 临时约束)\n * - `'corners'`:只有角手柄等比,边手柄仍单维自由\n * (片段自身「锁定宽高比」用这个 —— 若边手柄也等比,\n * 就会出现「拖右边、上下也跟着变」的诡异手感)\n */\nexport function resizeTransform(\n start: ClipTransform,\n handle: HandleKey,\n pointer: Point,\n keepAspect: boolean | 'corners' = false,\n): ClipTransform {\n const local = toLocalPoint(pointer, start)\n const hw = start.w / 2\n const hh = start.h / 2\n\n // 局部坐标下的四条边;拖动某条边只改它,其余固定不动(= 固定对角/对边)\n let left = -hw\n let right = hw\n let top = -hh\n let bottom = hh\n\n if (handle.includes('w')) left = Math.min(local.x, right - MIN_TRANSFORM_SIZE)\n if (handle.includes('e')) right = Math.max(local.x, left + MIN_TRANSFORM_SIZE)\n if (handle.includes('n')) top = Math.min(local.y, bottom - MIN_TRANSFORM_SIZE)\n if (handle.includes('s')) bottom = Math.max(local.y, top + MIN_TRANSFORM_SIZE)\n\n let nextW = right - left\n let nextH = bottom - top\n\n const isCorner = handle.length === 2\n const aspectActive = keepAspect === true || (keepAspect === 'corners' && isCorner)\n\n if (aspectActive && start.w > 0 && start.h > 0) {\n const ratio = start.w / start.h\n if (isCorner) {\n // 以宽为准反算高,再按被拖动的方向贴住不动的对边\n nextH = nextW / ratio\n if (handle.includes('n')) top = bottom - nextH\n else bottom = top + nextH\n } else if (handle === 'e' || handle === 'w') {\n nextH = nextW / ratio\n top = -nextH / 2\n bottom = nextH / 2\n } else {\n nextW = nextH * ratio\n left = -nextW / 2\n right = nextW / 2\n }\n }\n\n nextW = Math.max(MIN_TRANSFORM_SIZE, right - left)\n nextH = Math.max(MIN_TRANSFORM_SIZE, bottom - top)\n\n // 新中心(局部坐标)→ 世界坐标 → 左上角\n const localCenter = { x: (left + right) / 2, y: (top + bottom) / 2 }\n const worldCenter = fromLocalPoint(localCenter, start)\n return {\n x: worldCenter.x - nextW / 2,\n y: worldCenter.y - nextH / 2,\n w: nextW,\n h: nextH,\n angle: start.angle,\n }\n}\n\n/** 把角度归一化到 (-180, 180],避免旋转多圈后数值无限增大 */\nexport function normalizeAngle(deg: number): number {\n let a = deg % 360\n if (a > 180) a -= 360\n if (a <= -180) a += 360\n return a\n}\n\n/** 按手柄方位给出鼠标光标(已考虑素材自身旋转) */\nexport function handleCursor(handle: HandleKey | 'rotate', angleDeg: number): string {\n if (handle === 'rotate') return 'grab'\n const base: Record<HandleKey, number> = {\n n: 0,\n ne: 45,\n e: 90,\n se: 135,\n s: 180,\n sw: 225,\n w: 270,\n nw: 315,\n }\n const cursors = ['ns-resize', 'nesw-resize', 'ew-resize', 'nwse-resize']\n // 把素材旋转叠加到手柄方位上,再落到 4 档光标\n const deg = ((base[handle] + angleDeg) % 180 + 180) % 180\n const index = Math.round(deg / 45) % 4\n return cursors[index] ?? 'default'\n}\n\n/** 两个尺寸是否近似相等(避免浮点噪声导致无谓的 store 写入) */\nexport function sameTransform(a: ClipTransform, b: ClipTransform, epsilon = 0.5): boolean {\n return (\n Math.abs(a.x - b.x) < epsilon &&\n Math.abs(a.y - b.y) < epsilon &&\n Math.abs(a.w - b.w) < epsilon &&\n Math.abs(a.h - b.h) < epsilon &&\n Math.abs(a.angle - b.angle) < epsilon\n )\n}\n","/**\n * 画布(工程)尺寸与比例。\n *\n * 为什么需要单独一层:时间轴里所有 `clip.transform` 都以工程分辨率为坐标系,\n * 换比例时如果不做变换,1280×720 里铺满的素材放到 1080×1920 画布里就会\n * 只剩顶部一条 —— 这正是「换了比例看起来没生效 / 画面全是黑的」的成因。\n *\n * 因此尺寸变更分两种语义:\n * - `scale`(保持构图):把所有素材按同样的比例缩放并平移,视觉上「整幅画面\n * 被重新装进新画布」,适合横屏切竖屏、改分辨率。\n * - `keep`(保持素材原样):只换画布,素材坐标原封不动,适合「画布留白变多」。\n *\n * 纯函数,不依赖 React。\n */\n\nimport type { ClipTransform, Project, TimelineClip } from '../types'\n\nexport interface AspectPreset {\n id: string\n label: string\n hint: string\n width: number\n height: number\n}\n\n/**\n * 常用比例预设。\n *\n * 尺寸都取该比例下「编辑器默认档」的常见值:横屏 720p、竖屏 720×1280、\n * 方形 1080×1080、竖版 1080×1920、横版 1920×1080。用户也可以自定义。\n */\nexport const ASPECT_PRESETS: AspectPreset[] = [\n { id: '16:9', label: '16:9', hint: '横屏 1280×720', width: 1280, height: 720 },\n { id: '9:16', label: '9:16', hint: '竖屏 720×1280', width: 720, height: 1280 },\n { id: '1:1', label: '1:1', hint: '方形 1080×1080', width: 1080, height: 1080 },\n { id: '4:3', label: '4:3', hint: '标准 960×720', width: 960, height: 720 },\n { id: '3:4', label: '3:4', hint: '竖版 720×960', width: 720, height: 960 },\n { id: '4:5', label: '4:5', hint: '社交媒体 864×1080', width: 864, height: 1080 },\n { id: '21:9', label: '21:9', hint: '宽银幕 1680×720', width: 1680, height: 720 },\n]\n\nexport type SizeChangeMode = 'scale' | 'keep'\n\n/** 求最大公约数(用于把尺寸化简成比例) */\nfunction gcd(a: number, b: number): number {\n let x = Math.abs(Math.round(a))\n let y = Math.abs(Math.round(b))\n while (y > 0) {\n const t = x % y\n x = y\n y = t\n }\n return x || 1\n}\n\n/** `1920×1080` → `16:9`;无法化简时返回 `1920:1080` 形式 */\nexport function ratioLabel(width: number, height: number): string {\n if (width <= 0 || height <= 0) return '—'\n const divisor = gcd(width, height)\n return `${width / divisor}:${height / divisor}`\n}\n\n/** 保留两位小数,避免属性面板里出现 123.456789 */\nfunction round2(n: number): number {\n return Math.round(n * 100) / 100\n}\n\n/**\n * 把素材几何从 `from` 画布映射到 `to` 画布。\n *\n * 用**同一个**缩放系数(取宽高比中较小者),保证素材不变形:\n * 16:9 → 9:16 时画面按高度缩小、水平居中,两侧留黑 —— 与剪辑软件里\n * 「按比例适配」的行为一致。若分别按宽、高缩放,人物会被横向拉扁。\n */\nexport function remapTransform(\n transform: ClipTransform,\n from: { width: number; height: number },\n to: { width: number; height: number },\n): ClipTransform {\n const fromW = Math.max(1, from.width)\n const fromH = Math.max(1, from.height)\n const scale = Math.min(to.width / fromW, to.height / fromH)\n const centerX = (transform.x + transform.w / 2) * scale + (to.width - fromW * scale) / 2\n const centerY = (transform.y + transform.h / 2) * scale + (to.height - fromH * scale) / 2\n const w = transform.w * scale\n const h = transform.h * scale\n return {\n x: round2(centerX - w / 2),\n y: round2(centerY - h / 2),\n w: round2(w),\n h: round2(h),\n angle: transform.angle,\n }\n}\n\n/**\n * 根据模式生成新的 clips 数组。\n *\n * @param mode `scale` = 素材随画布等比缩放;`keep` = 素材坐标不动\n */\nexport function remapClipsForSize(\n clips: TimelineClip[],\n from: { width: number; height: number },\n to: { width: number; height: number },\n mode: SizeChangeMode,\n): TimelineClip[] {\n if (mode === 'keep') return clips\n return clips.map((clip) => ({\n ...clip,\n transform: remapTransform(clip.transform, from, to),\n }))\n}\n\n/** 判断工程是否已是非标尺寸(不在预设里) */\nexport function isCustomSize(project: Project): boolean {\n return !ASPECT_PRESETS.some((p) => p.width === project.width && p.height === project.height)\n}\n\n/** 尺寸合法化:不小于 16,偶数(编码器要求) */\nexport function normalizeSize(width: number, height: number): { width: number; height: number } {\n const even = (n: number) => Math.max(16, Math.round(n / 2) * 2)\n return { width: even(width), height: even(height) }\n}\n"],"mappings":"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"}
|