@cut-workbench/core 0.3.1 → 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.
- package/dist/chunks/{canvas-size-IzoEg2cq.cjs → canvas-size-DIABzEW2.cjs} +209 -302
- package/dist/chunks/canvas-size-DIABzEW2.cjs.map +1 -0
- package/dist/chunks/{canvas-size-iWL8zZPl.js → canvas-size-Dlnng74w.js} +168 -171
- 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/rolldown-runtime-B2SfzNBj.cjs +17 -0
- package/dist/chunks/rolldown-runtime-D8jQpDG7.js +12 -0
- package/dist/chunks/{snap-CeYuTN6d.cjs → snap-Cfk15JVg.cjs} +3 -23
- package/dist/chunks/{snap-CeYuTN6d.cjs.map → snap-Cfk15JVg.cjs.map} +1 -1
- package/dist/chunks/{snap-Dc022smD.js → snap-DpkVPVJN.js} +3 -17
- package/dist/chunks/{snap-Dc022smD.js.map → snap-DpkVPVJN.js.map} +1 -1
- package/dist/chunks/srt-D539yJ-n.js +51 -0
- package/dist/chunks/srt-D539yJ-n.js.map +1 -0
- package/dist/chunks/srt-DaL36mXN.cjs +73 -0
- package/dist/chunks/srt-DaL36mXN.cjs.map +1 -0
- 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-CYEd91qN.cjs +196 -0
- package/dist/chunks/time-CYEd91qN.cjs.map +1 -0
- package/dist/chunks/time-ChF1GwCy.js +113 -0
- package/dist/chunks/time-ChF1GwCy.js.map +1 -0
- package/dist/chunks/{waveform-Bhvv2t7-.cjs → waveform-BYwDdKpC.cjs} +150 -31
- package/dist/chunks/waveform-BYwDdKpC.cjs.map +1 -0
- package/dist/chunks/{waveform-CWU8Fa-n.js → waveform-uyGNE01H.js} +111 -22
- package/dist/chunks/waveform-uyGNE01H.js.map +1 -0
- package/dist/editor/create-editor.d.ts +8 -106
- package/dist/editor/editor-store.d.ts +4 -0
- package/dist/editor/handle-types.d.ts +256 -0
- package/dist/engine/index.cjs +1185 -455
- package/dist/engine/index.cjs.map +1 -1
- package/dist/engine/index.d.ts +3 -0
- package/dist/engine/index.js +1169 -446
- package/dist/engine/index.js.map +1 -1
- package/dist/engine/webav/export-runner.d.ts +61 -0
- package/dist/engine/webav/exporter.d.ts +3 -1
- package/dist/engine/webav/import-files.d.ts +58 -0
- package/dist/engine/webav/import-pipeline.d.ts +23 -1
- package/dist/engine/webav/types.d.ts +15 -0
- package/dist/errors.d.ts +12 -0
- package/dist/export/types.d.ts +49 -0
- package/dist/index.cjs +83 -68
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.js +47 -43
- 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/ports/host-bridge.d.ts +7 -0
- package/dist/project/file-storage.d.ts +23 -0
- package/dist/project/schema.d.ts +16 -4
- package/dist/timeline/index.cjs +24 -19
- package/dist/timeline/index.d.ts +1 -0
- package/dist/timeline/index.js +4 -3
- 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 +3 -2
- package/dist/chunks/canvas-size-IzoEg2cq.cjs.map +0 -1
- package/dist/chunks/canvas-size-iWL8zZPl.js.map +0 -1
- package/dist/chunks/waveform-Bhvv2t7-.cjs.map +0 -1
- package/dist/chunks/waveform-CWU8Fa-n.js.map +0 -1
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
let nanoid = require("nanoid");
|
|
2
|
+
// src/constants.ts
|
|
3
|
+
/**
|
|
4
|
+
* cut-core 自有常量。
|
|
5
|
+
*
|
|
6
|
+
* 只放「逻辑层必须知道」的数值:默认时长、默认音量、工程 schema 版本、
|
|
7
|
+
* 历史栈深度。UI 相关的量(缩放档位、面板高度、吸附像素阈值)留在宿主侧,
|
|
8
|
+
* 避免组件库被时间轴界面的调参牵着走。
|
|
9
|
+
*/
|
|
10
|
+
/** 图片 / 文字素材默认时长(微秒) */
|
|
11
|
+
var DEFAULT_STILL_DURATION_US = 5e6;
|
|
12
|
+
/** 默认音量,避免多轨直接相加削波 */
|
|
13
|
+
var DEFAULT_CLIP_VOLUME = .8;
|
|
14
|
+
/** 工程 schema 版本 */
|
|
15
|
+
var PROJECT_SCHEMA_VERSION = 1;
|
|
16
|
+
/** 撤销栈最大深度 */
|
|
17
|
+
var MAX_HISTORY_STEPS = 50;
|
|
18
|
+
/**
|
|
19
|
+
* 连续同质操作的合并时间窗口(毫秒)。
|
|
20
|
+
*
|
|
21
|
+
* 拖动滑块 / 画布内拖拽素材时会连续触发大量更新;窗口内相同 mergeKey 的记录
|
|
22
|
+
* 会被合并成一步撤销。取值兼顾「连续拖动算一次」与「停顿后再改算新一步」。
|
|
23
|
+
*/
|
|
24
|
+
var HISTORY_MERGE_WINDOW_MS = 800;
|
|
25
|
+
// src/lib/id.ts
|
|
26
|
+
/**
|
|
27
|
+
* 生成短 id。scope 仅用于可读性(例如 `clip_xxx`),不影响唯一性。
|
|
28
|
+
*/
|
|
29
|
+
function createId(scope) {
|
|
30
|
+
const id = (0, nanoid.nanoid)(10);
|
|
31
|
+
return scope ? `${scope}_${id}` : id;
|
|
32
|
+
}
|
|
33
|
+
Object.defineProperty(exports, "DEFAULT_CLIP_VOLUME", {
|
|
34
|
+
enumerable: true,
|
|
35
|
+
get: function() {
|
|
36
|
+
return DEFAULT_CLIP_VOLUME;
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
Object.defineProperty(exports, "DEFAULT_STILL_DURATION_US", {
|
|
40
|
+
enumerable: true,
|
|
41
|
+
get: function() {
|
|
42
|
+
return DEFAULT_STILL_DURATION_US;
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
Object.defineProperty(exports, "HISTORY_MERGE_WINDOW_MS", {
|
|
46
|
+
enumerable: true,
|
|
47
|
+
get: function() {
|
|
48
|
+
return HISTORY_MERGE_WINDOW_MS;
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
Object.defineProperty(exports, "MAX_HISTORY_STEPS", {
|
|
52
|
+
enumerable: true,
|
|
53
|
+
get: function() {
|
|
54
|
+
return MAX_HISTORY_STEPS;
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
Object.defineProperty(exports, "PROJECT_SCHEMA_VERSION", {
|
|
58
|
+
enumerable: true,
|
|
59
|
+
get: function() {
|
|
60
|
+
return PROJECT_SCHEMA_VERSION;
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
Object.defineProperty(exports, "createId", {
|
|
64
|
+
enumerable: true,
|
|
65
|
+
get: function() {
|
|
66
|
+
return createId;
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
//# sourceMappingURL=id-DaCxD4-5.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"id-DaCxD4-5.cjs","names":[],"sources":["../../src/constants.ts","../../src/lib/id.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"],"mappings":";;;;;;;;;;AASA,IAAa,4BAA4B;;AAGzC,IAAa,sBAAsB;;AAGnC,IAAa,yBAAyB;;AAGtC,IAAa,oBAAoB;;;;;;;AAQjC,IAAa,0BAA0B;;;;;ACrBvC,SAAgB,SAAS,OAAwB;CAC/C,MAAM,MAAA,GAAK,OAAA,OAAA,CAAO,EAAE;CACpB,OAAO,QAAQ,GAAG,MAAM,GAAG,OAAO;AACpC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// \0rolldown/runtime.js
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __exportAll = (all, no_symbols) => {
|
|
4
|
+
let target = {};
|
|
5
|
+
for (var name in all) __defProp(target, name, {
|
|
6
|
+
get: all[name],
|
|
7
|
+
enumerable: true
|
|
8
|
+
});
|
|
9
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
10
|
+
return target;
|
|
11
|
+
};
|
|
12
|
+
Object.defineProperty(exports, "__exportAll", {
|
|
13
|
+
enumerable: true,
|
|
14
|
+
get: function() {
|
|
15
|
+
return __exportAll;
|
|
16
|
+
}
|
|
17
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// \0rolldown/runtime.js
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __exportAll = (all, no_symbols) => {
|
|
4
|
+
let target = {};
|
|
5
|
+
for (var name in all) __defProp(target, name, {
|
|
6
|
+
get: all[name],
|
|
7
|
+
enumerable: true
|
|
8
|
+
});
|
|
9
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
10
|
+
return target;
|
|
11
|
+
};
|
|
12
|
+
export { __exportAll as t };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const
|
|
1
|
+
const require_time = require("./time-CYEd91qN.cjs");
|
|
2
2
|
// src/timeline/snap.ts
|
|
3
3
|
var DEFAULT_THRESHOLD_PX = 15;
|
|
4
4
|
var EMPTY_SNAP = {
|
|
@@ -9,7 +9,7 @@ var EMPTY_SNAP = {
|
|
|
9
9
|
hits: []
|
|
10
10
|
};
|
|
11
11
|
function pxOf(us, pxPerSecond) {
|
|
12
|
-
return
|
|
12
|
+
return require_time.usToPx(us, pxPerSecond);
|
|
13
13
|
}
|
|
14
14
|
/**
|
|
15
15
|
* 计算吸附结果。
|
|
@@ -117,20 +117,6 @@ function computeSnapWithHysteresis(input, releaseThresholdPx = 8) {
|
|
|
117
117
|
hits: [hit]
|
|
118
118
|
};
|
|
119
119
|
}
|
|
120
|
-
/** 二分查找:在已排序的时间数组中找最接近的值 */
|
|
121
|
-
function nearestSorted(sortedUs, targetUs) {
|
|
122
|
-
if (sortedUs.length === 0) return null;
|
|
123
|
-
let lo = 0;
|
|
124
|
-
let hi = sortedUs.length - 1;
|
|
125
|
-
while (lo < hi) {
|
|
126
|
-
const mid = lo + hi >> 1;
|
|
127
|
-
if ((sortedUs[mid] ?? 0) < targetUs) lo = mid + 1;
|
|
128
|
-
else hi = mid;
|
|
129
|
-
}
|
|
130
|
-
const hiVal = sortedUs[lo] ?? 0;
|
|
131
|
-
const loVal = sortedUs[Math.max(0, lo - 1)] ?? hiVal;
|
|
132
|
-
return Math.abs(hiVal - targetUs) <= Math.abs(targetUs - loVal) ? hiVal : loVal;
|
|
133
|
-
}
|
|
134
120
|
Object.defineProperty(exports, "EMPTY_SNAP", {
|
|
135
121
|
enumerable: true,
|
|
136
122
|
get: function() {
|
|
@@ -149,11 +135,5 @@ Object.defineProperty(exports, "computeSnapWithHysteresis", {
|
|
|
149
135
|
return computeSnapWithHysteresis;
|
|
150
136
|
}
|
|
151
137
|
});
|
|
152
|
-
Object.defineProperty(exports, "nearestSorted", {
|
|
153
|
-
enumerable: true,
|
|
154
|
-
get: function() {
|
|
155
|
-
return nearestSorted;
|
|
156
|
-
}
|
|
157
|
-
});
|
|
158
138
|
|
|
159
|
-
//# sourceMappingURL=snap-
|
|
139
|
+
//# sourceMappingURL=snap-Cfk15JVg.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"snap-
|
|
1
|
+
{"version":3,"file":"snap-Cfk15JVg.cjs","names":["usToPx"],"sources":["../../src/timeline/snap.ts"],"sourcesContent":["/**\n * 吸附算法(纯函数)。\n *\n * 设计要点:\n * - 候选点在**像素**空间比较,避免缩放变化导致阈值语义漂移。\n * - 一次拖拽内可同时吸附「起点」和「终点」,但返回结果必须保留「是哪条边被吸住」。\n * 这是修一个真实 bug 的关键:旧实现把两条边都换算成「新的起点」,\n * 右侧裁剪时起点是固定的,于是 `duration = snappedStart - originStart`\n * 会算出「目标点与当前终点的差值」,片段被压成最短时长(视觉上闪到左边)。\n * - 吸附结果用「位移量 deltaUs」表达:无论调用方改的是 start 还是 duration,\n * 都只需要把 delta 加到对应的边所在的量上。\n * - 滞后(hysteresis):已吸附后,只有偏离目标超过 `releaseThresholdPx` 才脱离,\n * 且释放阈值必须**大于**吸附阈值,否则「吸住」在数学上不可能成立\n * (旧实现释放阈值 8px < 吸附阈值 15px,滞后分支实际永远走不到)。\n */\n\nimport type { Px } from './time'\nimport { usToPx } from './time'\n\nexport type SnapTargetKind = 'clip-start' | 'clip-end' | 'playhead' | 'zero' | 'track-end'\n\nexport interface SnapTarget {\n us: number\n kind: SnapTargetKind\n /** 产生该目标的实体 id(clip / track),用于 UI 高亮 */\n sourceId?: string\n}\n\nexport type SnapEdge = 'start' | 'end'\n\nexport interface SnapInput {\n /** 被拖动区间的起点(微秒) */\n startUs: number\n /** 被拖动区间的时长(微秒) */\n durationUs: number\n targets: SnapTarget[]\n /** 每秒像素 */\n pxPerSecond: number\n thresholdPx?: number\n /** 上一帧的吸附结果(用于滞后判断) */\n previous?: SnapResult | null\n /**\n * 参与吸附的边。默认两端都参与。\n *\n * 裁剪时只让被拖动的边参与:右侧裁剪传 ['end']。\n * 旧实现即使只传 ['end'] 也会把结果换算成「新起点」,导致整段位移,已修正。\n */\n edges?: SnapEdge[]\n}\n\nexport interface SnapEdgeHit {\n /** 被吸附的是拖动区间的哪一端 */\n edge: SnapEdge\n /** 吸附到的目标时间(微秒) */\n targetUs: number\n kind: SnapTargetKind\n sourceId?: string\n /** 该目标对应的吸附线像素位置 */\n linePx: Px\n /** 该边吸附前的原始位置(微秒) */\n edgeUs: number\n /** 需要施加的位移:targetUs - edgeUs */\n deltaUs: number\n /** 吸附前该边与目标的像素距离 */\n distPx: number\n /** 命中时拖动区间的新起点(微秒),便于 move 模式直接使用 */\n nextStartUs: number\n}\n\nexport interface SnapResult {\n /** 命中吸附的边;null 表示未吸附 */\n edge: SnapEdge | null\n /** 吸附目标时间;未吸附为 null */\n targetUs: number | null\n /** 需要施加的位移(微秒);未吸附为 0 */\n deltaUs: number\n /** 需要绘制的辅助线(像素) */\n guidePx: Px[]\n /** 所有命中的边(可能两端同时命中不同目标) */\n hits: SnapEdgeHit[]\n}\n\nconst DEFAULT_THRESHOLD_PX = 15\n\nexport const EMPTY_SNAP: SnapResult = {\n edge: null,\n targetUs: null,\n deltaUs: 0,\n guidePx: [],\n hits: [],\n}\n\nfunction pxOf(us: number, pxPerSecond: number): number {\n return usToPx(us, pxPerSecond)\n}\n\n/**\n * 计算吸附结果。\n *\n * @param input 拖动区间 + 目标 + 缩放\n * @returns 命中的边、位移量、辅助线;未命中时 edge 为 null\n */\nexport function computeSnap(input: SnapInput): SnapResult {\n const { startUs, durationUs, targets, pxPerSecond } = input\n const threshold = input.thresholdPx ?? DEFAULT_THRESHOLD_PX\n const endUs = startUs + durationUs\n const activeEdges = new Set<SnapEdge>(input.edges ?? ['start', 'end'])\n const candidates: Array<{ edge: SnapEdge; us: number }> = [\n { edge: 'start', us: startUs },\n { edge: 'end', us: endUs },\n ]\n const edges = candidates.filter((edgeInfo) => activeEdges.has(edgeInfo.edge))\n\n const hits: SnapEdgeHit[] = []\n\n for (const edgeInfo of edges) {\n const edgePx = pxOf(edgeInfo.us, pxPerSecond)\n let best: { target: SnapTarget; distPx: number; px: number } | null = null\n\n for (const target of targets) {\n const targetPx = pxOf(target.us, pxPerSecond)\n const distPx = Math.abs(targetPx - edgePx)\n if (distPx > threshold) continue\n if (best == null || distPx < best.distPx) {\n best = { target, distPx, px: targetPx }\n }\n }\n\n if (best == null) continue\n\n const deltaUs = best.target.us - edgeInfo.us\n hits.push({\n edge: edgeInfo.edge,\n targetUs: best.target.us,\n kind: best.target.kind,\n sourceId: best.target.sourceId,\n linePx: best.px as Px,\n edgeUs: edgeInfo.us,\n deltaUs,\n distPx: best.distPx,\n nextStartUs: startUs + deltaUs,\n })\n }\n\n if (hits.length === 0) return EMPTY_SNAP\n\n let primary = hits[0]!\n for (const hit of hits) {\n if (hit.distPx < primary.distPx) primary = hit\n }\n\n return {\n edge: primary.edge,\n targetUs: primary.targetUs,\n deltaUs: primary.deltaUs,\n guidePx: uniquePx(hits.map((h) => h.linePx)),\n hits,\n }\n}\n\nfunction uniquePx(list: Px[]): Px[] {\n const seen = new Set<number>()\n const out: Px[] = []\n for (const p of list) {\n const key = Math.round(p * 2) / 2\n if (seen.has(key)) continue\n seen.add(key)\n out.push(p)\n }\n return out\n}\n\n/**\n * 带滞后的吸附。\n *\n * 若当前帧没有命中任何目标,但上一帧吸附在同一目标上、且该边偏离目标仍未超过\n * `releaseThresholdPx`,则维持原吸附。`releaseThresholdPx` 必须大于吸附阈值,\n * 否则这个分支永远不可能生效(旧实现就是如此)。\n */\nexport function computeSnapWithHysteresis(\n input: SnapInput,\n releaseThresholdPx = 8,\n): SnapResult {\n const fresh = computeSnap(input)\n const prev = input.previous\n if (fresh.edge != null) return fresh\n if (prev == null || prev.edge == null || prev.targetUs == null) return fresh\n\n const active = new Set<SnapEdge>(input.edges ?? ['start', 'end'])\n if (!active.has(prev.edge)) return fresh\n\n const edgeUs = prev.edge === 'start' ? input.startUs : input.startUs + input.durationUs\n const targetUs = prev.targetUs\n const distPx = Math.abs(pxOf(targetUs, input.pxPerSecond) - pxOf(edgeUs, input.pxPerSecond))\n if (distPx > releaseThresholdPx) return fresh\n\n const previousHit =\n prev.hits.find((h) => h.edge === prev.edge && h.targetUs === targetUs) ?? prev.hits[0]\n const linePx = pxOf(targetUs, input.pxPerSecond) as Px\n const hit: SnapEdgeHit = {\n edge: prev.edge,\n targetUs,\n kind: previousHit?.kind ?? 'zero',\n sourceId: previousHit?.sourceId,\n linePx,\n edgeUs,\n deltaUs: targetUs - edgeUs,\n distPx,\n nextStartUs: input.startUs + (targetUs - edgeUs),\n }\n\n return {\n edge: prev.edge,\n targetUs,\n deltaUs: targetUs - edgeUs,\n guidePx: [linePx],\n hits: [hit],\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { p as usToPx } from "./time-ChF1GwCy.js";
|
|
2
2
|
// src/timeline/snap.ts
|
|
3
3
|
var DEFAULT_THRESHOLD_PX = 15;
|
|
4
4
|
var EMPTY_SNAP = {
|
|
@@ -117,20 +117,6 @@ function computeSnapWithHysteresis(input, releaseThresholdPx = 8) {
|
|
|
117
117
|
hits: [hit]
|
|
118
118
|
};
|
|
119
119
|
}
|
|
120
|
-
|
|
121
|
-
function nearestSorted(sortedUs, targetUs) {
|
|
122
|
-
if (sortedUs.length === 0) return null;
|
|
123
|
-
let lo = 0;
|
|
124
|
-
let hi = sortedUs.length - 1;
|
|
125
|
-
while (lo < hi) {
|
|
126
|
-
const mid = lo + hi >> 1;
|
|
127
|
-
if ((sortedUs[mid] ?? 0) < targetUs) lo = mid + 1;
|
|
128
|
-
else hi = mid;
|
|
129
|
-
}
|
|
130
|
-
const hiVal = sortedUs[lo] ?? 0;
|
|
131
|
-
const loVal = sortedUs[Math.max(0, lo - 1)] ?? hiVal;
|
|
132
|
-
return Math.abs(hiVal - targetUs) <= Math.abs(targetUs - loVal) ? hiVal : loVal;
|
|
133
|
-
}
|
|
134
|
-
export { nearestSorted as i, computeSnap as n, computeSnapWithHysteresis as r, EMPTY_SNAP as t };
|
|
120
|
+
export { computeSnap as n, computeSnapWithHysteresis as r, EMPTY_SNAP as t };
|
|
135
121
|
|
|
136
|
-
//# sourceMappingURL=snap-
|
|
122
|
+
//# sourceMappingURL=snap-DpkVPVJN.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"snap-
|
|
1
|
+
{"version":3,"file":"snap-DpkVPVJN.js","names":[],"sources":["../../src/timeline/snap.ts"],"sourcesContent":["/**\n * 吸附算法(纯函数)。\n *\n * 设计要点:\n * - 候选点在**像素**空间比较,避免缩放变化导致阈值语义漂移。\n * - 一次拖拽内可同时吸附「起点」和「终点」,但返回结果必须保留「是哪条边被吸住」。\n * 这是修一个真实 bug 的关键:旧实现把两条边都换算成「新的起点」,\n * 右侧裁剪时起点是固定的,于是 `duration = snappedStart - originStart`\n * 会算出「目标点与当前终点的差值」,片段被压成最短时长(视觉上闪到左边)。\n * - 吸附结果用「位移量 deltaUs」表达:无论调用方改的是 start 还是 duration,\n * 都只需要把 delta 加到对应的边所在的量上。\n * - 滞后(hysteresis):已吸附后,只有偏离目标超过 `releaseThresholdPx` 才脱离,\n * 且释放阈值必须**大于**吸附阈值,否则「吸住」在数学上不可能成立\n * (旧实现释放阈值 8px < 吸附阈值 15px,滞后分支实际永远走不到)。\n */\n\nimport type { Px } from './time'\nimport { usToPx } from './time'\n\nexport type SnapTargetKind = 'clip-start' | 'clip-end' | 'playhead' | 'zero' | 'track-end'\n\nexport interface SnapTarget {\n us: number\n kind: SnapTargetKind\n /** 产生该目标的实体 id(clip / track),用于 UI 高亮 */\n sourceId?: string\n}\n\nexport type SnapEdge = 'start' | 'end'\n\nexport interface SnapInput {\n /** 被拖动区间的起点(微秒) */\n startUs: number\n /** 被拖动区间的时长(微秒) */\n durationUs: number\n targets: SnapTarget[]\n /** 每秒像素 */\n pxPerSecond: number\n thresholdPx?: number\n /** 上一帧的吸附结果(用于滞后判断) */\n previous?: SnapResult | null\n /**\n * 参与吸附的边。默认两端都参与。\n *\n * 裁剪时只让被拖动的边参与:右侧裁剪传 ['end']。\n * 旧实现即使只传 ['end'] 也会把结果换算成「新起点」,导致整段位移,已修正。\n */\n edges?: SnapEdge[]\n}\n\nexport interface SnapEdgeHit {\n /** 被吸附的是拖动区间的哪一端 */\n edge: SnapEdge\n /** 吸附到的目标时间(微秒) */\n targetUs: number\n kind: SnapTargetKind\n sourceId?: string\n /** 该目标对应的吸附线像素位置 */\n linePx: Px\n /** 该边吸附前的原始位置(微秒) */\n edgeUs: number\n /** 需要施加的位移:targetUs - edgeUs */\n deltaUs: number\n /** 吸附前该边与目标的像素距离 */\n distPx: number\n /** 命中时拖动区间的新起点(微秒),便于 move 模式直接使用 */\n nextStartUs: number\n}\n\nexport interface SnapResult {\n /** 命中吸附的边;null 表示未吸附 */\n edge: SnapEdge | null\n /** 吸附目标时间;未吸附为 null */\n targetUs: number | null\n /** 需要施加的位移(微秒);未吸附为 0 */\n deltaUs: number\n /** 需要绘制的辅助线(像素) */\n guidePx: Px[]\n /** 所有命中的边(可能两端同时命中不同目标) */\n hits: SnapEdgeHit[]\n}\n\nconst DEFAULT_THRESHOLD_PX = 15\n\nexport const EMPTY_SNAP: SnapResult = {\n edge: null,\n targetUs: null,\n deltaUs: 0,\n guidePx: [],\n hits: [],\n}\n\nfunction pxOf(us: number, pxPerSecond: number): number {\n return usToPx(us, pxPerSecond)\n}\n\n/**\n * 计算吸附结果。\n *\n * @param input 拖动区间 + 目标 + 缩放\n * @returns 命中的边、位移量、辅助线;未命中时 edge 为 null\n */\nexport function computeSnap(input: SnapInput): SnapResult {\n const { startUs, durationUs, targets, pxPerSecond } = input\n const threshold = input.thresholdPx ?? DEFAULT_THRESHOLD_PX\n const endUs = startUs + durationUs\n const activeEdges = new Set<SnapEdge>(input.edges ?? ['start', 'end'])\n const candidates: Array<{ edge: SnapEdge; us: number }> = [\n { edge: 'start', us: startUs },\n { edge: 'end', us: endUs },\n ]\n const edges = candidates.filter((edgeInfo) => activeEdges.has(edgeInfo.edge))\n\n const hits: SnapEdgeHit[] = []\n\n for (const edgeInfo of edges) {\n const edgePx = pxOf(edgeInfo.us, pxPerSecond)\n let best: { target: SnapTarget; distPx: number; px: number } | null = null\n\n for (const target of targets) {\n const targetPx = pxOf(target.us, pxPerSecond)\n const distPx = Math.abs(targetPx - edgePx)\n if (distPx > threshold) continue\n if (best == null || distPx < best.distPx) {\n best = { target, distPx, px: targetPx }\n }\n }\n\n if (best == null) continue\n\n const deltaUs = best.target.us - edgeInfo.us\n hits.push({\n edge: edgeInfo.edge,\n targetUs: best.target.us,\n kind: best.target.kind,\n sourceId: best.target.sourceId,\n linePx: best.px as Px,\n edgeUs: edgeInfo.us,\n deltaUs,\n distPx: best.distPx,\n nextStartUs: startUs + deltaUs,\n })\n }\n\n if (hits.length === 0) return EMPTY_SNAP\n\n let primary = hits[0]!\n for (const hit of hits) {\n if (hit.distPx < primary.distPx) primary = hit\n }\n\n return {\n edge: primary.edge,\n targetUs: primary.targetUs,\n deltaUs: primary.deltaUs,\n guidePx: uniquePx(hits.map((h) => h.linePx)),\n hits,\n }\n}\n\nfunction uniquePx(list: Px[]): Px[] {\n const seen = new Set<number>()\n const out: Px[] = []\n for (const p of list) {\n const key = Math.round(p * 2) / 2\n if (seen.has(key)) continue\n seen.add(key)\n out.push(p)\n }\n return out\n}\n\n/**\n * 带滞后的吸附。\n *\n * 若当前帧没有命中任何目标,但上一帧吸附在同一目标上、且该边偏离目标仍未超过\n * `releaseThresholdPx`,则维持原吸附。`releaseThresholdPx` 必须大于吸附阈值,\n * 否则这个分支永远不可能生效(旧实现就是如此)。\n */\nexport function computeSnapWithHysteresis(\n input: SnapInput,\n releaseThresholdPx = 8,\n): SnapResult {\n const fresh = computeSnap(input)\n const prev = input.previous\n if (fresh.edge != null) return fresh\n if (prev == null || prev.edge == null || prev.targetUs == null) return fresh\n\n const active = new Set<SnapEdge>(input.edges ?? ['start', 'end'])\n if (!active.has(prev.edge)) return fresh\n\n const edgeUs = prev.edge === 'start' ? input.startUs : input.startUs + input.durationUs\n const targetUs = prev.targetUs\n const distPx = Math.abs(pxOf(targetUs, input.pxPerSecond) - pxOf(edgeUs, input.pxPerSecond))\n if (distPx > releaseThresholdPx) return fresh\n\n const previousHit =\n prev.hits.find((h) => h.edge === prev.edge && h.targetUs === targetUs) ?? prev.hits[0]\n const linePx = pxOf(targetUs, input.pxPerSecond) as Px\n const hit: SnapEdgeHit = {\n edge: prev.edge,\n targetUs,\n kind: previousHit?.kind ?? 'zero',\n sourceId: previousHit?.sourceId,\n linePx,\n edgeUs,\n deltaUs: targetUs - edgeUs,\n distPx,\n nextStartUs: input.startUs + (targetUs - edgeUs),\n }\n\n return {\n edge: prev.edge,\n targetUs,\n deltaUs: targetUs - edgeUs,\n guidePx: [linePx],\n hits: [hit],\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-D8jQpDG7.js";
|
|
2
|
+
// src/media/srt.ts
|
|
3
|
+
var srt_exports = /* @__PURE__ */ __exportAll({
|
|
4
|
+
isValidSrt: () => isValidSrt,
|
|
5
|
+
parseSrt: () => parseSrt,
|
|
6
|
+
parseSrtDurationUs: () => parseSrtDurationUs
|
|
7
|
+
});
|
|
8
|
+
var TIME_RE = /(\d{2}):(\d{2}):(\d{2})[,.](\d{3})/g;
|
|
9
|
+
function toMs(h, m, s, ms) {
|
|
10
|
+
return Number(h) * 36e5 + Number(m) * 6e4 + Number(s) * 1e3 + Number(ms);
|
|
11
|
+
}
|
|
12
|
+
/** 解析 SRT 文本为 cue 列表;格式错误时返回空数组(调用方给出友好提示) */
|
|
13
|
+
function parseSrt(content) {
|
|
14
|
+
const blocks = content.replace(/\r\n/g, "\n").split(/\n{2,}/);
|
|
15
|
+
const cues = [];
|
|
16
|
+
for (const block of blocks) {
|
|
17
|
+
const lines = block.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
18
|
+
if (lines.length === 0) continue;
|
|
19
|
+
const timeLineIndex = lines.findIndex((l) => l.includes("-->"));
|
|
20
|
+
if (timeLineIndex < 0) continue;
|
|
21
|
+
const timeLine = lines[timeLineIndex] ?? "";
|
|
22
|
+
TIME_RE.lastIndex = 0;
|
|
23
|
+
const matches = Array.from(timeLine.matchAll(TIME_RE));
|
|
24
|
+
if (matches.length < 2) continue;
|
|
25
|
+
const [startMatch, endMatch] = matches;
|
|
26
|
+
if (startMatch == null || endMatch == null) continue;
|
|
27
|
+
const startMs = toMs(startMatch[1] ?? "0", startMatch[2] ?? "0", startMatch[3] ?? "0", startMatch[4] ?? "0");
|
|
28
|
+
const endMs = toMs(endMatch[1] ?? "0", endMatch[2] ?? "0", endMatch[3] ?? "0", endMatch[4] ?? "0");
|
|
29
|
+
if (endMs <= startMs) continue;
|
|
30
|
+
cues.push({
|
|
31
|
+
index: cues.length + 1,
|
|
32
|
+
startMs,
|
|
33
|
+
endMs,
|
|
34
|
+
text: lines.slice(timeLineIndex + 1).join("\n")
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
return cues;
|
|
38
|
+
}
|
|
39
|
+
/** 字幕总时长(微秒)。用于给字幕素材一个有限的 deration */
|
|
40
|
+
function parseSrtDurationUs(content) {
|
|
41
|
+
const cues = parseSrt(content);
|
|
42
|
+
const last = cues[cues.length - 1];
|
|
43
|
+
return last == null ? 0 : Math.round(last.endMs * 1e3);
|
|
44
|
+
}
|
|
45
|
+
/** 简单校验:至少一条 cue */
|
|
46
|
+
function isValidSrt(content) {
|
|
47
|
+
return parseSrt(content).length > 0;
|
|
48
|
+
}
|
|
49
|
+
export { srt_exports as i, parseSrt as n, parseSrtDurationUs as r, isValidSrt as t };
|
|
50
|
+
|
|
51
|
+
//# sourceMappingURL=srt-D539yJ-n.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"srt-D539yJ-n.js","names":[],"sources":["../../src/media/srt.ts"],"sourcesContent":["/**\n * SRT 解析工具(仅用于估算字幕素材时长、校验格式)。\n *\n * 真正的字幕渲染交给 EmbedSubtitlesClip,这里只做纯文本解析,\n * 从而让 UI 能在创建素材时立刻知道时长。\n */\n\nexport interface SrtCue {\n index: number\n startMs: number\n endMs: number\n text: string\n}\n\nconst TIME_RE = /(\\d{2}):(\\d{2}):(\\d{2})[,.](\\d{3})/g\n\nfunction toMs(h: string, m: string, s: string, ms: string): number {\n return Number(h) * 3_600_000 + Number(m) * 60_000 + Number(s) * 1_000 + Number(ms)\n}\n\n/** 解析 SRT 文本为 cue 列表;格式错误时返回空数组(调用方给出友好提示) */\nexport function parseSrt(content: string): SrtCue[] {\n const blocks = content.replace(/\\r\\n/g, '\\n').split(/\\n{2,}/)\n const cues: SrtCue[] = []\n for (const block of blocks) {\n const lines = block.split('\\n').map((l) => l.trim()).filter(Boolean)\n if (lines.length === 0) continue\n const timeLineIndex = lines.findIndex((l) => l.includes('-->'))\n if (timeLineIndex < 0) continue\n const timeLine = lines[timeLineIndex] ?? ''\n TIME_RE.lastIndex = 0\n const matches = Array.from(timeLine.matchAll(TIME_RE))\n if (matches.length < 2) continue\n const [startMatch, endMatch] = matches\n if (startMatch == null || endMatch == null) continue\n const startMs = toMs(startMatch[1] ?? '0', startMatch[2] ?? '0', startMatch[3] ?? '0', startMatch[4] ?? '0')\n const endMs = toMs(endMatch[1] ?? '0', endMatch[2] ?? '0', endMatch[3] ?? '0', endMatch[4] ?? '0')\n if (endMs <= startMs) continue\n cues.push({\n index: cues.length + 1,\n startMs,\n endMs,\n text: lines.slice(timeLineIndex + 1).join('\\n'),\n })\n }\n return cues\n}\n\n/** 字幕总时长(微秒)。用于给字幕素材一个有限的 deration */\nexport function parseSrtDurationUs(content: string): number {\n const cues = parseSrt(content)\n const last = cues[cues.length - 1]\n return last == null ? 0 : Math.round(last.endMs * 1000)\n}\n\n/** 简单校验:至少一条 cue */\nexport function isValidSrt(content: string): boolean {\n return parseSrt(content).length > 0\n}\n"],"mappings":";;;;;;;AAcA,IAAM,UAAU;AAEhB,SAAS,KAAK,GAAW,GAAW,GAAW,IAAoB;CACjE,OAAO,OAAO,CAAC,IAAI,OAAY,OAAO,CAAC,IAAI,MAAS,OAAO,CAAC,IAAI,MAAQ,OAAO,EAAE;AACnF;;AAGA,SAAgB,SAAS,SAA2B;CAClD,MAAM,SAAS,QAAQ,QAAQ,SAAS,IAAI,CAAC,CAAC,MAAM,QAAQ;CAC5D,MAAM,OAAiB,CAAC;CACxB,KAAK,MAAM,SAAS,QAAQ;EAC1B,MAAM,QAAQ,MAAM,MAAM,IAAI,CAAC,CAAC,KAAK,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC,OAAO,OAAO;EACnE,IAAI,MAAM,WAAW,GAAG;EACxB,MAAM,gBAAgB,MAAM,WAAW,MAAM,EAAE,SAAS,KAAK,CAAC;EAC9D,IAAI,gBAAgB,GAAG;EACvB,MAAM,WAAW,MAAM,kBAAkB;EACzC,QAAQ,YAAY;EACpB,MAAM,UAAU,MAAM,KAAK,SAAS,SAAS,OAAO,CAAC;EACrD,IAAI,QAAQ,SAAS,GAAG;EACxB,MAAM,CAAC,YAAY,YAAY;EAC/B,IAAI,cAAc,QAAQ,YAAY,MAAM;EAC5C,MAAM,UAAU,KAAK,WAAW,MAAM,KAAK,WAAW,MAAM,KAAK,WAAW,MAAM,KAAK,WAAW,MAAM,GAAG;EAC3G,MAAM,QAAQ,KAAK,SAAS,MAAM,KAAK,SAAS,MAAM,KAAK,SAAS,MAAM,KAAK,SAAS,MAAM,GAAG;EACjG,IAAI,SAAS,SAAS;EACtB,KAAK,KAAK;GACR,OAAO,KAAK,SAAS;GACrB;GACA;GACA,MAAM,MAAM,MAAM,gBAAgB,CAAC,CAAC,CAAC,KAAK,IAAI;EAChD,CAAC;CACH;CACA,OAAO;AACT;;AAGA,SAAgB,mBAAmB,SAAyB;CAC1D,MAAM,OAAO,SAAS,OAAO;CAC7B,MAAM,OAAO,KAAK,KAAK,SAAS;CAChC,OAAO,QAAQ,OAAO,IAAI,KAAK,MAAM,KAAK,QAAQ,GAAI;AACxD;;AAGA,SAAgB,WAAW,SAA0B;CACnD,OAAO,SAAS,OAAO,CAAC,CAAC,SAAS;AACpC"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// src/media/srt.ts
|
|
2
|
+
var srt_exports = /* @__PURE__ */ require("./rolldown-runtime-B2SfzNBj.cjs").__exportAll({
|
|
3
|
+
isValidSrt: () => isValidSrt,
|
|
4
|
+
parseSrt: () => parseSrt,
|
|
5
|
+
parseSrtDurationUs: () => parseSrtDurationUs
|
|
6
|
+
});
|
|
7
|
+
var TIME_RE = /(\d{2}):(\d{2}):(\d{2})[,.](\d{3})/g;
|
|
8
|
+
function toMs(h, m, s, ms) {
|
|
9
|
+
return Number(h) * 36e5 + Number(m) * 6e4 + Number(s) * 1e3 + Number(ms);
|
|
10
|
+
}
|
|
11
|
+
/** 解析 SRT 文本为 cue 列表;格式错误时返回空数组(调用方给出友好提示) */
|
|
12
|
+
function parseSrt(content) {
|
|
13
|
+
const blocks = content.replace(/\r\n/g, "\n").split(/\n{2,}/);
|
|
14
|
+
const cues = [];
|
|
15
|
+
for (const block of blocks) {
|
|
16
|
+
const lines = block.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
17
|
+
if (lines.length === 0) continue;
|
|
18
|
+
const timeLineIndex = lines.findIndex((l) => l.includes("-->"));
|
|
19
|
+
if (timeLineIndex < 0) continue;
|
|
20
|
+
const timeLine = lines[timeLineIndex] ?? "";
|
|
21
|
+
TIME_RE.lastIndex = 0;
|
|
22
|
+
const matches = Array.from(timeLine.matchAll(TIME_RE));
|
|
23
|
+
if (matches.length < 2) continue;
|
|
24
|
+
const [startMatch, endMatch] = matches;
|
|
25
|
+
if (startMatch == null || endMatch == null) continue;
|
|
26
|
+
const startMs = toMs(startMatch[1] ?? "0", startMatch[2] ?? "0", startMatch[3] ?? "0", startMatch[4] ?? "0");
|
|
27
|
+
const endMs = toMs(endMatch[1] ?? "0", endMatch[2] ?? "0", endMatch[3] ?? "0", endMatch[4] ?? "0");
|
|
28
|
+
if (endMs <= startMs) continue;
|
|
29
|
+
cues.push({
|
|
30
|
+
index: cues.length + 1,
|
|
31
|
+
startMs,
|
|
32
|
+
endMs,
|
|
33
|
+
text: lines.slice(timeLineIndex + 1).join("\n")
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return cues;
|
|
37
|
+
}
|
|
38
|
+
/** 字幕总时长(微秒)。用于给字幕素材一个有限的 deration */
|
|
39
|
+
function parseSrtDurationUs(content) {
|
|
40
|
+
const cues = parseSrt(content);
|
|
41
|
+
const last = cues[cues.length - 1];
|
|
42
|
+
return last == null ? 0 : Math.round(last.endMs * 1e3);
|
|
43
|
+
}
|
|
44
|
+
/** 简单校验:至少一条 cue */
|
|
45
|
+
function isValidSrt(content) {
|
|
46
|
+
return parseSrt(content).length > 0;
|
|
47
|
+
}
|
|
48
|
+
Object.defineProperty(exports, "isValidSrt", {
|
|
49
|
+
enumerable: true,
|
|
50
|
+
get: function() {
|
|
51
|
+
return isValidSrt;
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
Object.defineProperty(exports, "parseSrt", {
|
|
55
|
+
enumerable: true,
|
|
56
|
+
get: function() {
|
|
57
|
+
return parseSrt;
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
Object.defineProperty(exports, "parseSrtDurationUs", {
|
|
61
|
+
enumerable: true,
|
|
62
|
+
get: function() {
|
|
63
|
+
return parseSrtDurationUs;
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
Object.defineProperty(exports, "srt_exports", {
|
|
67
|
+
enumerable: true,
|
|
68
|
+
get: function() {
|
|
69
|
+
return srt_exports;
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
//# sourceMappingURL=srt-DaL36mXN.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"srt-DaL36mXN.cjs","names":[],"sources":["../../src/media/srt.ts"],"sourcesContent":["/**\n * SRT 解析工具(仅用于估算字幕素材时长、校验格式)。\n *\n * 真正的字幕渲染交给 EmbedSubtitlesClip,这里只做纯文本解析,\n * 从而让 UI 能在创建素材时立刻知道时长。\n */\n\nexport interface SrtCue {\n index: number\n startMs: number\n endMs: number\n text: string\n}\n\nconst TIME_RE = /(\\d{2}):(\\d{2}):(\\d{2})[,.](\\d{3})/g\n\nfunction toMs(h: string, m: string, s: string, ms: string): number {\n return Number(h) * 3_600_000 + Number(m) * 60_000 + Number(s) * 1_000 + Number(ms)\n}\n\n/** 解析 SRT 文本为 cue 列表;格式错误时返回空数组(调用方给出友好提示) */\nexport function parseSrt(content: string): SrtCue[] {\n const blocks = content.replace(/\\r\\n/g, '\\n').split(/\\n{2,}/)\n const cues: SrtCue[] = []\n for (const block of blocks) {\n const lines = block.split('\\n').map((l) => l.trim()).filter(Boolean)\n if (lines.length === 0) continue\n const timeLineIndex = lines.findIndex((l) => l.includes('-->'))\n if (timeLineIndex < 0) continue\n const timeLine = lines[timeLineIndex] ?? ''\n TIME_RE.lastIndex = 0\n const matches = Array.from(timeLine.matchAll(TIME_RE))\n if (matches.length < 2) continue\n const [startMatch, endMatch] = matches\n if (startMatch == null || endMatch == null) continue\n const startMs = toMs(startMatch[1] ?? '0', startMatch[2] ?? '0', startMatch[3] ?? '0', startMatch[4] ?? '0')\n const endMs = toMs(endMatch[1] ?? '0', endMatch[2] ?? '0', endMatch[3] ?? '0', endMatch[4] ?? '0')\n if (endMs <= startMs) continue\n cues.push({\n index: cues.length + 1,\n startMs,\n endMs,\n text: lines.slice(timeLineIndex + 1).join('\\n'),\n })\n }\n return cues\n}\n\n/** 字幕总时长(微秒)。用于给字幕素材一个有限的 deration */\nexport function parseSrtDurationUs(content: string): number {\n const cues = parseSrt(content)\n const last = cues[cues.length - 1]\n return last == null ? 0 : Math.round(last.endMs * 1000)\n}\n\n/** 简单校验:至少一条 cue */\nexport function isValidSrt(content: string): boolean {\n return parseSrt(content).length > 0\n}\n"],"mappings":";;;;;;AAcA,IAAM,UAAU;AAEhB,SAAS,KAAK,GAAW,GAAW,GAAW,IAAoB;CACjE,OAAO,OAAO,CAAC,IAAI,OAAY,OAAO,CAAC,IAAI,MAAS,OAAO,CAAC,IAAI,MAAQ,OAAO,EAAE;AACnF;;AAGA,SAAgB,SAAS,SAA2B;CAClD,MAAM,SAAS,QAAQ,QAAQ,SAAS,IAAI,CAAC,CAAC,MAAM,QAAQ;CAC5D,MAAM,OAAiB,CAAC;CACxB,KAAK,MAAM,SAAS,QAAQ;EAC1B,MAAM,QAAQ,MAAM,MAAM,IAAI,CAAC,CAAC,KAAK,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC,OAAO,OAAO;EACnE,IAAI,MAAM,WAAW,GAAG;EACxB,MAAM,gBAAgB,MAAM,WAAW,MAAM,EAAE,SAAS,KAAK,CAAC;EAC9D,IAAI,gBAAgB,GAAG;EACvB,MAAM,WAAW,MAAM,kBAAkB;EACzC,QAAQ,YAAY;EACpB,MAAM,UAAU,MAAM,KAAK,SAAS,SAAS,OAAO,CAAC;EACrD,IAAI,QAAQ,SAAS,GAAG;EACxB,MAAM,CAAC,YAAY,YAAY;EAC/B,IAAI,cAAc,QAAQ,YAAY,MAAM;EAC5C,MAAM,UAAU,KAAK,WAAW,MAAM,KAAK,WAAW,MAAM,KAAK,WAAW,MAAM,KAAK,WAAW,MAAM,GAAG;EAC3G,MAAM,QAAQ,KAAK,SAAS,MAAM,KAAK,SAAS,MAAM,KAAK,SAAS,MAAM,KAAK,SAAS,MAAM,GAAG;EACjG,IAAI,SAAS,SAAS;EACtB,KAAK,KAAK;GACR,OAAO,KAAK,SAAS;GACrB;GACA;GACA,MAAM,MAAM,MAAM,gBAAgB,CAAC,CAAC,CAAC,KAAK,IAAI;EAChD,CAAC;CACH;CACA,OAAO;AACT;;AAGA,SAAgB,mBAAmB,SAAyB;CAC1D,MAAM,OAAO,SAAS,OAAO;CAC7B,MAAM,OAAO,KAAK,KAAK,SAAS;CAChC,OAAO,QAAQ,OAAO,IAAI,KAAK,MAAM,KAAK,QAAQ,GAAI;AACxD;;AAGA,SAAgB,WAAW,SAA0B;CACnD,OAAO,SAAS,OAAO,CAAC,CAAC,SAAS;AACpC"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
const require_rolldown_runtime = require("./rolldown-runtime-B2SfzNBj.cjs");
|
|
2
|
+
const require_time = require("./time-CYEd91qN.cjs");
|
|
3
|
+
// src/media/thumbnails.ts
|
|
4
|
+
var thumbnails_exports = /* @__PURE__ */ require_rolldown_runtime.__exportAll({
|
|
5
|
+
generateThumbnails: () => generateThumbnails,
|
|
6
|
+
regenerateThumbnails: () => regenerateThumbnails,
|
|
7
|
+
releaseThumbnails: () => releaseThumbnails
|
|
8
|
+
});
|
|
9
|
+
var cache = /* @__PURE__ */ new Map();
|
|
10
|
+
/**
|
|
11
|
+
* 生成等间隔缩略图条。
|
|
12
|
+
*
|
|
13
|
+
* @param clip MP4Clip 实例(调用方负责生命周期)
|
|
14
|
+
* @param assetId 缓存 key
|
|
15
|
+
* @param count 期望张数
|
|
16
|
+
* @param durationUs 素材总时长,用来算 step
|
|
17
|
+
* @param width 单张宽度
|
|
18
|
+
*/
|
|
19
|
+
async function generateThumbnails(clip, assetId, count, durationUs, width = 100) {
|
|
20
|
+
const cached = cache.get(assetId);
|
|
21
|
+
if (cached) return cached;
|
|
22
|
+
return regenerateThumbnails(clip, assetId, count, durationUs, width);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* 绕过缓存重新抽帧。
|
|
26
|
+
*
|
|
27
|
+
* 重新打开工程后素材 id 不变,但旧的 `blob:` URL 已经失效;缓存 key 是 assetId,
|
|
28
|
+
* 直接走 `generateThumbnails` 会命中失效缓存(卡片显示破图/占位)。
|
|
29
|
+
* 这里先释放旧缓存再抽一次。
|
|
30
|
+
*/
|
|
31
|
+
async function regenerateThumbnails(clip, assetId, count, durationUs, width = 100) {
|
|
32
|
+
if (cache.has(assetId)) releaseThumbnails(assetId);
|
|
33
|
+
const stepUs = Math.max(Math.floor(durationUs / count), require_time.usToMs(200));
|
|
34
|
+
const raw = await clip.thumbnails(width, {
|
|
35
|
+
start: 0,
|
|
36
|
+
end: Math.max(0, durationUs - 1),
|
|
37
|
+
step: stepUs
|
|
38
|
+
});
|
|
39
|
+
const picked = [];
|
|
40
|
+
if (raw.length <= count) picked.push(...raw);
|
|
41
|
+
else {
|
|
42
|
+
const stride = raw.length / count;
|
|
43
|
+
for (let i = 0; i < count; i += 1) {
|
|
44
|
+
const item = raw[Math.min(raw.length - 1, Math.floor(i * stride))];
|
|
45
|
+
if (item) picked.push(item);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const frames = picked.map((item) => ({
|
|
49
|
+
ts: item.ts,
|
|
50
|
+
url: URL.createObjectURL(item.img)
|
|
51
|
+
}));
|
|
52
|
+
cache.set(assetId, frames);
|
|
53
|
+
return frames;
|
|
54
|
+
}
|
|
55
|
+
/** 释放某素材的缩略图 URL */
|
|
56
|
+
function releaseThumbnails(assetId) {
|
|
57
|
+
const frames = cache.get(assetId);
|
|
58
|
+
if (!frames) return;
|
|
59
|
+
for (const f of frames) URL.revokeObjectURL(f.url);
|
|
60
|
+
cache.delete(assetId);
|
|
61
|
+
}
|
|
62
|
+
Object.defineProperty(exports, "generateThumbnails", {
|
|
63
|
+
enumerable: true,
|
|
64
|
+
get: function() {
|
|
65
|
+
return generateThumbnails;
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
Object.defineProperty(exports, "releaseThumbnails", {
|
|
69
|
+
enumerable: true,
|
|
70
|
+
get: function() {
|
|
71
|
+
return releaseThumbnails;
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
Object.defineProperty(exports, "thumbnails_exports", {
|
|
75
|
+
enumerable: true,
|
|
76
|
+
get: function() {
|
|
77
|
+
return thumbnails_exports;
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
//# sourceMappingURL=thumbnails-DEjnao5r.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"thumbnails-DEjnao5r.cjs","names":["usToMs"],"sources":["../../src/media/thumbnails.ts"],"sourcesContent":["/**\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 return regenerateThumbnails(clip, assetId, count, durationUs, width)\r\n}\r\n\r\n/**\r\n * 绕过缓存重新抽帧。\r\n *\r\n * 重新打开工程后素材 id 不变,但旧的 `blob:` URL 已经失效;缓存 key 是 assetId,\r\n * 直接走 `generateThumbnails` 会命中失效缓存(卡片显示破图/占位)。\r\n * 这里先释放旧缓存再抽一次。\r\n */\r\nexport async function regenerateThumbnails(\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 if (cache.has(assetId)) releaseThumbnails(assetId)\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"],"mappings":";;;;;;;;AAeA,IAAM,wBAAQ,IAAI,IAA8B;;;;;;;;;;AAmBhD,eAAsB,mBACpB,MACA,SACA,OACA,YACA,QAAQ,KACmB;CAC3B,MAAM,SAAS,MAAM,IAAI,OAAO;CAChC,IAAI,QAAQ,OAAO;CACnB,OAAO,qBAAqB,MAAM,SAAS,OAAO,YAAY,KAAK;AACrE;;;;;;;;AASA,eAAsB,qBACpB,MACA,SACA,OACA,YACA,QAAQ,KACmB;CAC3B,IAAI,MAAM,IAAI,OAAO,GAAG,kBAAkB,OAAO;CAEjD,MAAM,SAAS,KAAK,IAAI,KAAK,MAAM,aAAa,KAAK,GAAGA,aAAAA,OAAO,GAAG,CAAC;CACnE,MAAM,MAAM,MAAM,KAAK,WAAW,OAAO;EACvC,OAAO;EACP,KAAK,KAAK,IAAI,GAAG,aAAa,CAAC;EAC/B,MAAM;CACR,CAAC;CAID,MAAM,SAA2C,CAAC;CAClD,IAAI,IAAI,UAAU,OAChB,OAAO,KAAK,GAAG,GAAG;MACb;EACL,MAAM,SAAS,IAAI,SAAS;EAC5B,KAAK,IAAI,IAAI,GAAG,IAAI,OAAO,KAAK,GAAG;GACjC,MAAM,OAAO,IAAI,KAAK,IAAI,IAAI,SAAS,GAAG,KAAK,MAAM,IAAI,MAAM,CAAC;GAChE,IAAI,MAAM,OAAO,KAAK,IAAI;EAC5B;CACF;CAEA,MAAM,SAAS,OAAO,KAAK,UAAU;EACnC,IAAI,KAAK;EACT,KAAK,IAAI,gBAAgB,KAAK,GAAG;CACnC,EAAE;CACF,MAAM,IAAI,SAAS,MAAM;CACzB,OAAO;AACT;;AAGA,SAAgB,kBAAkB,SAAuB;CACvD,MAAM,SAAS,MAAM,IAAI,OAAO;CAChC,IAAI,CAAC,QAAQ;CACb,KAAK,MAAM,KAAK,QAAQ,IAAI,gBAAgB,EAAE,GAAG;CACjD,MAAM,OAAO,OAAO;AACtB"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-D8jQpDG7.js";
|
|
2
|
+
import { f as usToMs } from "./time-ChF1GwCy.js";
|
|
3
|
+
// src/media/thumbnails.ts
|
|
4
|
+
var thumbnails_exports = /* @__PURE__ */ __exportAll({
|
|
5
|
+
generateThumbnails: () => generateThumbnails,
|
|
6
|
+
regenerateThumbnails: () => regenerateThumbnails,
|
|
7
|
+
releaseThumbnails: () => releaseThumbnails
|
|
8
|
+
});
|
|
9
|
+
var cache = /* @__PURE__ */ new Map();
|
|
10
|
+
/**
|
|
11
|
+
* 生成等间隔缩略图条。
|
|
12
|
+
*
|
|
13
|
+
* @param clip MP4Clip 实例(调用方负责生命周期)
|
|
14
|
+
* @param assetId 缓存 key
|
|
15
|
+
* @param count 期望张数
|
|
16
|
+
* @param durationUs 素材总时长,用来算 step
|
|
17
|
+
* @param width 单张宽度
|
|
18
|
+
*/
|
|
19
|
+
async function generateThumbnails(clip, assetId, count, durationUs, width = 100) {
|
|
20
|
+
const cached = cache.get(assetId);
|
|
21
|
+
if (cached) return cached;
|
|
22
|
+
return regenerateThumbnails(clip, assetId, count, durationUs, width);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* 绕过缓存重新抽帧。
|
|
26
|
+
*
|
|
27
|
+
* 重新打开工程后素材 id 不变,但旧的 `blob:` URL 已经失效;缓存 key 是 assetId,
|
|
28
|
+
* 直接走 `generateThumbnails` 会命中失效缓存(卡片显示破图/占位)。
|
|
29
|
+
* 这里先释放旧缓存再抽一次。
|
|
30
|
+
*/
|
|
31
|
+
async function regenerateThumbnails(clip, assetId, count, durationUs, width = 100) {
|
|
32
|
+
if (cache.has(assetId)) releaseThumbnails(assetId);
|
|
33
|
+
const stepUs = Math.max(Math.floor(durationUs / count), usToMs(200));
|
|
34
|
+
const raw = await clip.thumbnails(width, {
|
|
35
|
+
start: 0,
|
|
36
|
+
end: Math.max(0, durationUs - 1),
|
|
37
|
+
step: stepUs
|
|
38
|
+
});
|
|
39
|
+
const picked = [];
|
|
40
|
+
if (raw.length <= count) picked.push(...raw);
|
|
41
|
+
else {
|
|
42
|
+
const stride = raw.length / count;
|
|
43
|
+
for (let i = 0; i < count; i += 1) {
|
|
44
|
+
const item = raw[Math.min(raw.length - 1, Math.floor(i * stride))];
|
|
45
|
+
if (item) picked.push(item);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const frames = picked.map((item) => ({
|
|
49
|
+
ts: item.ts,
|
|
50
|
+
url: URL.createObjectURL(item.img)
|
|
51
|
+
}));
|
|
52
|
+
cache.set(assetId, frames);
|
|
53
|
+
return frames;
|
|
54
|
+
}
|
|
55
|
+
/** 释放某素材的缩略图 URL */
|
|
56
|
+
function releaseThumbnails(assetId) {
|
|
57
|
+
const frames = cache.get(assetId);
|
|
58
|
+
if (!frames) return;
|
|
59
|
+
for (const f of frames) URL.revokeObjectURL(f.url);
|
|
60
|
+
cache.delete(assetId);
|
|
61
|
+
}
|
|
62
|
+
export { releaseThumbnails as n, thumbnails_exports as r, generateThumbnails as t };
|
|
63
|
+
|
|
64
|
+
//# sourceMappingURL=thumbnails-qH0dX_Zs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"thumbnails-qH0dX_Zs.js","names":[],"sources":["../../src/media/thumbnails.ts"],"sourcesContent":["/**\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 return regenerateThumbnails(clip, assetId, count, durationUs, width)\r\n}\r\n\r\n/**\r\n * 绕过缓存重新抽帧。\r\n *\r\n * 重新打开工程后素材 id 不变,但旧的 `blob:` URL 已经失效;缓存 key 是 assetId,\r\n * 直接走 `generateThumbnails` 会命中失效缓存(卡片显示破图/占位)。\r\n * 这里先释放旧缓存再抽一次。\r\n */\r\nexport async function regenerateThumbnails(\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 if (cache.has(assetId)) releaseThumbnails(assetId)\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"],"mappings":";;;;;;;;AAeA,IAAM,wBAAQ,IAAI,IAA8B;;;;;;;;;;AAmBhD,eAAsB,mBACpB,MACA,SACA,OACA,YACA,QAAQ,KACmB;CAC3B,MAAM,SAAS,MAAM,IAAI,OAAO;CAChC,IAAI,QAAQ,OAAO;CACnB,OAAO,qBAAqB,MAAM,SAAS,OAAO,YAAY,KAAK;AACrE;;;;;;;;AASA,eAAsB,qBACpB,MACA,SACA,OACA,YACA,QAAQ,KACmB;CAC3B,IAAI,MAAM,IAAI,OAAO,GAAG,kBAAkB,OAAO;CAEjD,MAAM,SAAS,KAAK,IAAI,KAAK,MAAM,aAAa,KAAK,GAAG,OAAO,GAAG,CAAC;CACnE,MAAM,MAAM,MAAM,KAAK,WAAW,OAAO;EACvC,OAAO;EACP,KAAK,KAAK,IAAI,GAAG,aAAa,CAAC;EAC/B,MAAM;CACR,CAAC;CAID,MAAM,SAA2C,CAAC;CAClD,IAAI,IAAI,UAAU,OAChB,OAAO,KAAK,GAAG,GAAG;MACb;EACL,MAAM,SAAS,IAAI,SAAS;EAC5B,KAAK,IAAI,IAAI,GAAG,IAAI,OAAO,KAAK,GAAG;GACjC,MAAM,OAAO,IAAI,KAAK,IAAI,IAAI,SAAS,GAAG,KAAK,MAAM,IAAI,MAAM,CAAC;GAChE,IAAI,MAAM,OAAO,KAAK,IAAI;EAC5B;CACF;CAEA,MAAM,SAAS,OAAO,KAAK,UAAU;EACnC,IAAI,KAAK;EACT,KAAK,IAAI,gBAAgB,KAAK,GAAG;CACnC,EAAE;CACF,MAAM,IAAI,SAAS,MAAM;CACzB,OAAO;AACT;;AAGA,SAAgB,kBAAkB,SAAuB;CACvD,MAAM,SAAS,MAAM,IAAI,OAAO;CAChC,IAAI,CAAC,QAAQ;CACb,KAAK,MAAM,KAAK,QAAQ,IAAI,gBAAgB,EAAE,GAAG;CACjD,MAAM,OAAO,OAAO;AACtB"}
|