@cut-workbench/core 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/dist/chunks/{canvas-size-iWL8zZPl.js → canvas-size-CM2oKApi.js} +3 -144
  2. package/dist/chunks/{canvas-size-iWL8zZPl.js.map → canvas-size-CM2oKApi.js.map} +1 -1
  3. package/dist/chunks/{canvas-size-IzoEg2cq.cjs → canvas-size-Ct61OYjW.cjs} +26 -287
  4. package/dist/chunks/canvas-size-Ct61OYjW.cjs.map +1 -0
  5. package/dist/chunks/import-pipeline-A8KkqtZT.cjs +610 -0
  6. package/dist/chunks/import-pipeline-A8KkqtZT.cjs.map +1 -0
  7. package/dist/chunks/import-pipeline-DLIdgrIb.js +521 -0
  8. package/dist/chunks/import-pipeline-DLIdgrIb.js.map +1 -0
  9. package/dist/chunks/rolldown-runtime-B2SfzNBj.cjs +17 -0
  10. package/dist/chunks/rolldown-runtime-D8jQpDG7.js +12 -0
  11. package/dist/chunks/{snap-Dc022smD.js → snap-CBV9G4xq.js} +2 -2
  12. package/dist/chunks/{snap-Dc022smD.js.map → snap-CBV9G4xq.js.map} +1 -1
  13. package/dist/chunks/{snap-CeYuTN6d.cjs → snap-DyIuO2hU.cjs} +3 -3
  14. package/dist/chunks/{snap-CeYuTN6d.cjs.map → snap-DyIuO2hU.cjs.map} +1 -1
  15. package/dist/chunks/srt-D539yJ-n.js +51 -0
  16. package/dist/chunks/srt-D539yJ-n.js.map +1 -0
  17. package/dist/chunks/srt-DaL36mXN.cjs +73 -0
  18. package/dist/chunks/srt-DaL36mXN.cjs.map +1 -0
  19. package/dist/chunks/time-C_gvkVl1.js +145 -0
  20. package/dist/chunks/time-C_gvkVl1.js.map +1 -0
  21. package/dist/chunks/time-CnMfSVYX.cjs +264 -0
  22. package/dist/chunks/time-CnMfSVYX.cjs.map +1 -0
  23. package/dist/chunks/{waveform-CWU8Fa-n.js → waveform-DJkRtMct.js} +82 -12
  24. package/dist/chunks/waveform-DJkRtMct.js.map +1 -0
  25. package/dist/chunks/{waveform-Bhvv2t7-.cjs → waveform-Ie-G5Lle.cjs} +121 -21
  26. package/dist/chunks/waveform-Ie-G5Lle.cjs.map +1 -0
  27. package/dist/editor/create-editor.d.ts +8 -106
  28. package/dist/editor/editor-store.d.ts +4 -0
  29. package/dist/editor/handle-types.d.ts +256 -0
  30. package/dist/engine/index.cjs +961 -823
  31. package/dist/engine/index.cjs.map +1 -1
  32. package/dist/engine/index.d.ts +3 -0
  33. package/dist/engine/index.js +935 -803
  34. package/dist/engine/index.js.map +1 -1
  35. package/dist/engine/webav/export-runner.d.ts +61 -0
  36. package/dist/engine/webav/import-files.d.ts +43 -0
  37. package/dist/engine/webav/import-pipeline.d.ts +13 -1
  38. package/dist/errors.d.ts +12 -0
  39. package/dist/export/types.d.ts +49 -0
  40. package/dist/index.cjs +75 -65
  41. package/dist/index.cjs.map +1 -1
  42. package/dist/index.d.ts +5 -0
  43. package/dist/index.js +46 -43
  44. package/dist/index.js.map +1 -1
  45. package/dist/ports/host-bridge.d.ts +7 -0
  46. package/dist/project/file-storage.d.ts +23 -0
  47. package/dist/timeline/index.cjs +17 -16
  48. package/dist/timeline/index.js +3 -2
  49. package/package.json +78 -77
  50. package/dist/chunks/canvas-size-IzoEg2cq.cjs.map +0 -1
  51. package/dist/chunks/waveform-Bhvv2t7-.cjs.map +0 -1
  52. package/dist/chunks/waveform-CWU8Fa-n.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"snap-Dc022smD.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\n/** 二分查找:在已排序的时间数组中找最接近的值 */\nexport function nearestSorted(sortedUs: number[], targetUs: number): number | null {\n if (sortedUs.length === 0) return null\n let lo = 0\n let hi = sortedUs.length - 1\n while (lo < hi) {\n const mid = (lo + hi) >> 1\n if ((sortedUs[mid] ?? 0) < targetUs) lo = mid + 1\n else hi = mid\n }\n const hiVal = sortedUs[lo] ?? 0\n const loVal = sortedUs[Math.max(0, lo - 1)] ?? hiVal\n return Math.abs(hiVal - targetUs) <= Math.abs(targetUs - loVal) ? hiVal : loVal\n}\n"],"mappings":";;AAkFA,IAAM,uBAAuB;AAE7B,IAAa,aAAyB;CACpC,MAAM;CACN,UAAU;CACV,SAAS;CACT,SAAS,CAAC;CACV,MAAM,CAAC;AACT;AAEA,SAAS,KAAK,IAAY,aAA6B;CACrD,OAAO,OAAO,IAAI,WAAW;AAC/B;;;;;;;AAQA,SAAgB,YAAY,OAA8B;CACxD,MAAM,EAAE,SAAS,YAAY,SAAS,gBAAgB;CACtD,MAAM,YAAY,MAAM,eAAe;CACvC,MAAM,QAAQ,UAAU;CACxB,MAAM,cAAc,IAAI,IAAc,MAAM,SAAS,CAAC,SAAS,KAAK,CAAC;CAKrE,MAAM,QAAQ,CAHZ;EAAE,MAAM;EAAS,IAAI;CAAQ,GAC7B;EAAE,MAAM;EAAO,IAAI;CAAM,CAEb,CAAA,CAAW,QAAQ,aAAa,YAAY,IAAI,SAAS,IAAI,CAAC;CAE5E,MAAM,OAAsB,CAAC;CAE7B,KAAK,MAAM,YAAY,OAAO;EAC5B,MAAM,SAAS,KAAK,SAAS,IAAI,WAAW;EAC5C,IAAI,OAAkE;EAEtE,KAAK,MAAM,UAAU,SAAS;GAC5B,MAAM,WAAW,KAAK,OAAO,IAAI,WAAW;GAC5C,MAAM,SAAS,KAAK,IAAI,WAAW,MAAM;GACzC,IAAI,SAAS,WAAW;GACxB,IAAI,QAAQ,QAAQ,SAAS,KAAK,QAChC,OAAO;IAAE;IAAQ;IAAQ,IAAI;GAAS;EAE1C;EAEA,IAAI,QAAQ,MAAM;EAElB,MAAM,UAAU,KAAK,OAAO,KAAK,SAAS;EAC1C,KAAK,KAAK;GACR,MAAM,SAAS;GACf,UAAU,KAAK,OAAO;GACtB,MAAM,KAAK,OAAO;GAClB,UAAU,KAAK,OAAO;GACtB,QAAQ,KAAK;GACb,QAAQ,SAAS;GACjB;GACA,QAAQ,KAAK;GACb,aAAa,UAAU;EACzB,CAAC;CACH;CAEA,IAAI,KAAK,WAAW,GAAG,OAAO;CAE9B,IAAI,UAAU,KAAK;CACnB,KAAK,MAAM,OAAO,MAChB,IAAI,IAAI,SAAS,QAAQ,QAAQ,UAAU;CAG7C,OAAO;EACL,MAAM,QAAQ;EACd,UAAU,QAAQ;EAClB,SAAS,QAAQ;EACjB,SAAS,SAAS,KAAK,KAAK,MAAM,EAAE,MAAM,CAAC;EAC3C;CACF;AACF;AAEA,SAAS,SAAS,MAAkB;CAClC,MAAM,uBAAO,IAAI,IAAY;CAC7B,MAAM,MAAY,CAAC;CACnB,KAAK,MAAM,KAAK,MAAM;EACpB,MAAM,MAAM,KAAK,MAAM,IAAI,CAAC,IAAI;EAChC,IAAI,KAAK,IAAI,GAAG,GAAG;EACnB,KAAK,IAAI,GAAG;EACZ,IAAI,KAAK,CAAC;CACZ;CACA,OAAO;AACT;;;;;;;;AASA,SAAgB,0BACd,OACA,qBAAqB,GACT;CACZ,MAAM,QAAQ,YAAY,KAAK;CAC/B,MAAM,OAAO,MAAM;CACnB,IAAI,MAAM,QAAQ,MAAM,OAAO;CAC/B,IAAI,QAAQ,QAAQ,KAAK,QAAQ,QAAQ,KAAK,YAAY,MAAM,OAAO;CAGvE,IAAI,CAAC,IADc,IAAc,MAAM,SAAS,CAAC,SAAS,KAAK,CAC1D,CAAA,CAAO,IAAI,KAAK,IAAI,GAAG,OAAO;CAEnC,MAAM,SAAS,KAAK,SAAS,UAAU,MAAM,UAAU,MAAM,UAAU,MAAM;CAC7E,MAAM,WAAW,KAAK;CACtB,MAAM,SAAS,KAAK,IAAI,KAAK,UAAU,MAAM,WAAW,IAAI,KAAK,QAAQ,MAAM,WAAW,CAAC;CAC3F,IAAI,SAAS,oBAAoB,OAAO;CAExC,MAAM,cACJ,KAAK,KAAK,MAAM,MAAM,EAAE,SAAS,KAAK,QAAQ,EAAE,aAAa,QAAQ,KAAK,KAAK,KAAK;CACtF,MAAM,SAAS,KAAK,UAAU,MAAM,WAAW;CAC/C,MAAM,MAAmB;EACvB,MAAM,KAAK;EACX;EACA,MAAM,aAAa,QAAQ;EAC3B,UAAU,aAAa;EACvB;EACA;EACA,SAAS,WAAW;EACpB;EACA,aAAa,MAAM,WAAW,WAAW;CAC3C;CAEA,OAAO;EACL,MAAM,KAAK;EACX;EACA,SAAS,WAAW;EACpB,SAAS,CAAC,MAAM;EAChB,MAAM,CAAC,GAAG;CACZ;AACF;;AAGA,SAAgB,cAAc,UAAoB,UAAiC;CACjF,IAAI,SAAS,WAAW,GAAG,OAAO;CAClC,IAAI,KAAK;CACT,IAAI,KAAK,SAAS,SAAS;CAC3B,OAAO,KAAK,IAAI;EACd,MAAM,MAAO,KAAK,MAAO;EACzB,KAAK,SAAS,QAAQ,KAAK,UAAU,KAAK,MAAM;OAC3C,KAAK;CACZ;CACA,MAAM,QAAQ,SAAS,OAAO;CAC9B,MAAM,QAAQ,SAAS,KAAK,IAAI,GAAG,KAAK,CAAC,MAAM;CAC/C,OAAO,KAAK,IAAI,QAAQ,QAAQ,KAAK,KAAK,IAAI,WAAW,KAAK,IAAI,QAAQ;AAC5E"}
1
+ {"version":3,"file":"snap-CBV9G4xq.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\n/** 二分查找:在已排序的时间数组中找最接近的值 */\nexport function nearestSorted(sortedUs: number[], targetUs: number): number | null {\n if (sortedUs.length === 0) return null\n let lo = 0\n let hi = sortedUs.length - 1\n while (lo < hi) {\n const mid = (lo + hi) >> 1\n if ((sortedUs[mid] ?? 0) < targetUs) lo = mid + 1\n else hi = mid\n }\n const hiVal = sortedUs[lo] ?? 0\n const loVal = sortedUs[Math.max(0, lo - 1)] ?? hiVal\n return Math.abs(hiVal - targetUs) <= Math.abs(targetUs - loVal) ? hiVal : loVal\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- const require_canvas_size = require("./canvas-size-IzoEg2cq.cjs");
1
+ const require_time = require("./time-CnMfSVYX.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 require_canvas_size.usToPx(us, pxPerSecond);
12
+ return require_time.usToPx(us, pxPerSecond);
13
13
  }
14
14
  /**
15
15
  * 计算吸附结果。
@@ -156,4 +156,4 @@ Object.defineProperty(exports, "nearestSorted", {
156
156
  }
157
157
  });
158
158
 
159
- //# sourceMappingURL=snap-CeYuTN6d.cjs.map
159
+ //# sourceMappingURL=snap-DyIuO2hU.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"snap-CeYuTN6d.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\n/** 二分查找:在已排序的时间数组中找最接近的值 */\nexport function nearestSorted(sortedUs: number[], targetUs: number): number | null {\n if (sortedUs.length === 0) return null\n let lo = 0\n let hi = sortedUs.length - 1\n while (lo < hi) {\n const mid = (lo + hi) >> 1\n if ((sortedUs[mid] ?? 0) < targetUs) lo = mid + 1\n else hi = mid\n }\n const hiVal = sortedUs[lo] ?? 0\n const loVal = sortedUs[Math.max(0, lo - 1)] ?? hiVal\n return Math.abs(hiVal - targetUs) <= Math.abs(targetUs - loVal) ? hiVal : loVal\n}\n"],"mappings":";;AAkFA,IAAM,uBAAuB;AAE7B,IAAa,aAAyB;CACpC,MAAM;CACN,UAAU;CACV,SAAS;CACT,SAAS,CAAC;CACV,MAAM,CAAC;AACT;AAEA,SAAS,KAAK,IAAY,aAA6B;CACrD,OAAOA,oBAAAA,OAAO,IAAI,WAAW;AAC/B;;;;;;;AAQA,SAAgB,YAAY,OAA8B;CACxD,MAAM,EAAE,SAAS,YAAY,SAAS,gBAAgB;CACtD,MAAM,YAAY,MAAM,eAAe;CACvC,MAAM,QAAQ,UAAU;CACxB,MAAM,cAAc,IAAI,IAAc,MAAM,SAAS,CAAC,SAAS,KAAK,CAAC;CAKrE,MAAM,QAAQ,CAHZ;EAAE,MAAM;EAAS,IAAI;CAAQ,GAC7B;EAAE,MAAM;EAAO,IAAI;CAAM,CAEb,CAAA,CAAW,QAAQ,aAAa,YAAY,IAAI,SAAS,IAAI,CAAC;CAE5E,MAAM,OAAsB,CAAC;CAE7B,KAAK,MAAM,YAAY,OAAO;EAC5B,MAAM,SAAS,KAAK,SAAS,IAAI,WAAW;EAC5C,IAAI,OAAkE;EAEtE,KAAK,MAAM,UAAU,SAAS;GAC5B,MAAM,WAAW,KAAK,OAAO,IAAI,WAAW;GAC5C,MAAM,SAAS,KAAK,IAAI,WAAW,MAAM;GACzC,IAAI,SAAS,WAAW;GACxB,IAAI,QAAQ,QAAQ,SAAS,KAAK,QAChC,OAAO;IAAE;IAAQ;IAAQ,IAAI;GAAS;EAE1C;EAEA,IAAI,QAAQ,MAAM;EAElB,MAAM,UAAU,KAAK,OAAO,KAAK,SAAS;EAC1C,KAAK,KAAK;GACR,MAAM,SAAS;GACf,UAAU,KAAK,OAAO;GACtB,MAAM,KAAK,OAAO;GAClB,UAAU,KAAK,OAAO;GACtB,QAAQ,KAAK;GACb,QAAQ,SAAS;GACjB;GACA,QAAQ,KAAK;GACb,aAAa,UAAU;EACzB,CAAC;CACH;CAEA,IAAI,KAAK,WAAW,GAAG,OAAO;CAE9B,IAAI,UAAU,KAAK;CACnB,KAAK,MAAM,OAAO,MAChB,IAAI,IAAI,SAAS,QAAQ,QAAQ,UAAU;CAG7C,OAAO;EACL,MAAM,QAAQ;EACd,UAAU,QAAQ;EAClB,SAAS,QAAQ;EACjB,SAAS,SAAS,KAAK,KAAK,MAAM,EAAE,MAAM,CAAC;EAC3C;CACF;AACF;AAEA,SAAS,SAAS,MAAkB;CAClC,MAAM,uBAAO,IAAI,IAAY;CAC7B,MAAM,MAAY,CAAC;CACnB,KAAK,MAAM,KAAK,MAAM;EACpB,MAAM,MAAM,KAAK,MAAM,IAAI,CAAC,IAAI;EAChC,IAAI,KAAK,IAAI,GAAG,GAAG;EACnB,KAAK,IAAI,GAAG;EACZ,IAAI,KAAK,CAAC;CACZ;CACA,OAAO;AACT;;;;;;;;AASA,SAAgB,0BACd,OACA,qBAAqB,GACT;CACZ,MAAM,QAAQ,YAAY,KAAK;CAC/B,MAAM,OAAO,MAAM;CACnB,IAAI,MAAM,QAAQ,MAAM,OAAO;CAC/B,IAAI,QAAQ,QAAQ,KAAK,QAAQ,QAAQ,KAAK,YAAY,MAAM,OAAO;CAGvE,IAAI,CAAC,IADc,IAAc,MAAM,SAAS,CAAC,SAAS,KAAK,CAC1D,CAAA,CAAO,IAAI,KAAK,IAAI,GAAG,OAAO;CAEnC,MAAM,SAAS,KAAK,SAAS,UAAU,MAAM,UAAU,MAAM,UAAU,MAAM;CAC7E,MAAM,WAAW,KAAK;CACtB,MAAM,SAAS,KAAK,IAAI,KAAK,UAAU,MAAM,WAAW,IAAI,KAAK,QAAQ,MAAM,WAAW,CAAC;CAC3F,IAAI,SAAS,oBAAoB,OAAO;CAExC,MAAM,cACJ,KAAK,KAAK,MAAM,MAAM,EAAE,SAAS,KAAK,QAAQ,EAAE,aAAa,QAAQ,KAAK,KAAK,KAAK;CACtF,MAAM,SAAS,KAAK,UAAU,MAAM,WAAW;CAC/C,MAAM,MAAmB;EACvB,MAAM,KAAK;EACX;EACA,MAAM,aAAa,QAAQ;EAC3B,UAAU,aAAa;EACvB;EACA;EACA,SAAS,WAAW;EACpB;EACA,aAAa,MAAM,WAAW,WAAW;CAC3C;CAEA,OAAO;EACL,MAAM,KAAK;EACX;EACA,SAAS,WAAW;EACpB,SAAS,CAAC,MAAM;EAChB,MAAM,CAAC,GAAG;CACZ;AACF;;AAGA,SAAgB,cAAc,UAAoB,UAAiC;CACjF,IAAI,SAAS,WAAW,GAAG,OAAO;CAClC,IAAI,KAAK;CACT,IAAI,KAAK,SAAS,SAAS;CAC3B,OAAO,KAAK,IAAI;EACd,MAAM,MAAO,KAAK,MAAO;EACzB,KAAK,SAAS,QAAQ,KAAK,UAAU,KAAK,MAAM;OAC3C,KAAK;CACZ;CACA,MAAM,QAAQ,SAAS,OAAO;CAC9B,MAAM,QAAQ,SAAS,KAAK,IAAI,GAAG,KAAK,CAAC,MAAM;CAC/C,OAAO,KAAK,IAAI,QAAQ,QAAQ,KAAK,KAAK,IAAI,WAAW,KAAK,IAAI,QAAQ;AAC5E"}
1
+ {"version":3,"file":"snap-DyIuO2hU.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\n/** 二分查找:在已排序的时间数组中找最接近的值 */\nexport function nearestSorted(sortedUs: number[], targetUs: number): number | null {\n if (sortedUs.length === 0) return null\n let lo = 0\n let hi = sortedUs.length - 1\n while (lo < hi) {\n const mid = (lo + hi) >> 1\n if ((sortedUs[mid] ?? 0) < targetUs) lo = mid + 1\n else hi = mid\n }\n const hiVal = sortedUs[lo] ?? 0\n const loVal = sortedUs[Math.max(0, lo - 1)] ?? hiVal\n return Math.abs(hiVal - targetUs) <= Math.abs(targetUs - loVal) ? hiVal : loVal\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,145 @@
1
+ import { nanoid } from "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 = nanoid(10);
31
+ return scope ? `${scope}_${id}` : id;
32
+ }
33
+ // src/timeline/time.ts
34
+ var US_PER_SECOND = 1e6;
35
+ var US_PER_MS = 1e3;
36
+ /** 秒 → 微秒 */
37
+ function secToUs(sec) {
38
+ return Math.round(sec * US_PER_SECOND);
39
+ }
40
+ /** 微秒 → 秒 */
41
+ function usToSec(us) {
42
+ return us / US_PER_SECOND;
43
+ }
44
+ /** 毫秒 → 微秒 */
45
+ function msToUs(ms) {
46
+ return Math.round(ms * US_PER_MS);
47
+ }
48
+ /** 微秒 → 毫秒 */
49
+ function usToMs(us) {
50
+ return Math.round(us / US_PER_MS);
51
+ }
52
+ /** 数字 → 微秒(用于从 DOM/WebAV 回调拿到的裸数字) */
53
+ function asUs(n) {
54
+ return Math.round(n);
55
+ }
56
+ /** 数字 → 像素 */
57
+ function asPx(n) {
58
+ return n;
59
+ }
60
+ /**
61
+ * 微秒 → 像素。
62
+ * @param us 时间(微秒)
63
+ * @param pxPerSecond 每秒对应多少像素
64
+ */
65
+ function usToPx(us, pxPerSecond) {
66
+ return us / US_PER_SECOND * pxPerSecond;
67
+ }
68
+ /**
69
+ * 像素 → 微秒。
70
+ * @param px 像素
71
+ * @param pxPerSecond 每秒对应多少像素
72
+ */
73
+ function pxToUs(px, pxPerSecond) {
74
+ return Math.round(px / pxPerSecond * US_PER_SECOND);
75
+ }
76
+ /** 微秒 → `HH:MM:SS.mmm` */
77
+ function formatTimecode(us, showMs = true) {
78
+ const totalMs = Math.floor(Math.max(0, us) / 1e3);
79
+ const ms = totalMs % 1e3;
80
+ const totalSec = Math.floor(totalMs / 1e3);
81
+ const sec = totalSec % 60;
82
+ const min = Math.floor(totalSec / 60) % 60;
83
+ const hour = Math.floor(totalSec / 3600);
84
+ const pad = (n, len = 2) => String(n).padStart(len, "0");
85
+ const base = `${pad(hour)}:${pad(min)}:${pad(sec)}`;
86
+ return showMs ? `${base}.${pad(ms, 3)}` : base;
87
+ }
88
+ /** 微秒 → `MM:SS` 或 `HH:MM:SS`,列表展示用 */
89
+ function formatDuration(us) {
90
+ if (!Number.isFinite(us)) return "∞";
91
+ const totalSec = Math.max(0, Math.round(us / US_PER_SECOND));
92
+ const sec = totalSec % 60;
93
+ const min = Math.floor(totalSec / 60) % 60;
94
+ const hour = Math.floor(totalSec / 3600);
95
+ const pad = (n) => String(n).padStart(2, "0");
96
+ return hour > 0 ? `${hour}:${pad(min)}:${pad(sec)}` : `${pad(min)}:${pad(sec)}`;
97
+ }
98
+ var TICK_STEPS_US = [
99
+ 1e5,
100
+ 2e5,
101
+ 5e5,
102
+ 1e6,
103
+ 2e6,
104
+ 5e6,
105
+ 1e7,
106
+ 15e6,
107
+ 3e7,
108
+ 6e7,
109
+ 12e7,
110
+ 3e8,
111
+ 6e8
112
+ ];
113
+ /**
114
+ * 根据缩放级别生成刻度尺。
115
+ *
116
+ * @param pxPerSecond 每秒像素
117
+ * @param viewportWidthPx 可视宽度(像素)
118
+ * @param scrollLeftPx 横向滚动偏移(像素)
119
+ * @returns 主刻度(带文字)与次刻度
120
+ */
121
+ function buildTicks(pxPerSecond, viewportWidthPx, scrollLeftPx = 0) {
122
+ const startUs = pxToUs(scrollLeftPx, pxPerSecond);
123
+ const endUs = pxToUs(scrollLeftPx + viewportWidthPx, pxPerSecond);
124
+ const minLabelSpacingPx = 72;
125
+ const step = TICK_STEPS_US.find((s) => usToPx(s, pxPerSecond) >= minLabelSpacingPx) ?? TICK_STEPS_US[TICK_STEPS_US.length - 1];
126
+ const minorStep = step / 5;
127
+ const first = Math.floor(startUs / step) * step;
128
+ const ticks = [];
129
+ for (let us = first; us <= endUs + step; us += minorStep) {
130
+ if (us < 0) continue;
131
+ ticks.push({
132
+ us,
133
+ px: usToPx(us, pxPerSecond),
134
+ major: us % step === 0
135
+ });
136
+ }
137
+ return ticks;
138
+ }
139
+ /** 保证值落在 [min, max] */
140
+ function clamp(value, min, max) {
141
+ return Math.min(max, Math.max(min, value));
142
+ }
143
+ export { DEFAULT_STILL_DURATION_US as _, buildTicks as a, PROJECT_SCHEMA_VERSION as b, formatTimecode as c, secToUs as d, usToMs as f, DEFAULT_CLIP_VOLUME as g, createId as h, asUs as i, msToUs as l, usToSec as m, US_PER_SECOND as n, clamp as o, usToPx as p, asPx as r, formatDuration as s, US_PER_MS as t, pxToUs as u, HISTORY_MERGE_WINDOW_MS as v, MAX_HISTORY_STEPS as y };
144
+
145
+ //# sourceMappingURL=time-C_gvkVl1.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"time-C_gvkVl1.js","names":[],"sources":["../../src/constants.ts","../../src/lib/id.ts","../../src/timeline/time.ts"],"sourcesContent":["/**\n * cut-core 自有常量。\n *\n * 只放「逻辑层必须知道」的数值:默认时长、默认音量、工程 schema 版本、\n * 历史栈深度。UI 相关的量(缩放档位、面板高度、吸附像素阈值)留在宿主侧,\n * 避免组件库被时间轴界面的调参牵着走。\n */\n\n/** 图片 / 文字素材默认时长(微秒) */\nexport const DEFAULT_STILL_DURATION_US = 5_000_000\n\n/** 默认音量,避免多轨直接相加削波 */\nexport const DEFAULT_CLIP_VOLUME = 0.8\n\n/** 工程 schema 版本 */\nexport const PROJECT_SCHEMA_VERSION = 1\n\n/** 撤销栈最大深度 */\nexport const MAX_HISTORY_STEPS = 50\n\n/**\n * 连续同质操作的合并时间窗口(毫秒)。\n *\n * 拖动滑块 / 画布内拖拽素材时会连续触发大量更新;窗口内相同 mergeKey 的记录\n * 会被合并成一步撤销。取值兼顾「连续拖动算一次」与「停顿后再改算新一步」。\n */\nexport const HISTORY_MERGE_WINDOW_MS = 800\n","import { nanoid } from 'nanoid'\n\n/**\n * 生成短 id。scope 仅用于可读性(例如 `clip_xxx`),不影响唯一性。\n */\nexport function createId(scope?: string): string {\n const id = nanoid(10)\n return scope ? `${scope}_${id}` : id\n}\n","/**\n * 时间与像素的单位换算。\n *\n * 内部统一使用微秒(µs)。为避免中途混用毫秒/秒,这里定义 branded type:\n * - `Us` 微秒\n * - `Ms` 毫秒\n * - `Px` 像素\n *\n * 只在函数边界处做 cast,业务代码不允许出现裸 number 参与时间计算。\n */\n\ndeclare const usBrand: unique symbol\ndeclare const msBrand: unique symbol\ndeclare const pxBrand: unique symbol\n\nexport type Us = number & { readonly [usBrand]: true }\nexport type Ms = number & { readonly [msBrand]: true }\nexport type Px = number & { readonly [pxBrand]: true }\n\nexport const US_PER_SECOND = 1_000_000\nexport const US_PER_MS = 1_000\n\n/** 秒 → 微秒 */\nexport function secToUs(sec: number): Us {\n return Math.round(sec * US_PER_SECOND) as Us\n}\n\n/** 微秒 → 秒 */\nexport function usToSec(us: number): number {\n return us / US_PER_SECOND\n}\n\n/** 毫秒 → 微秒 */\nexport function msToUs(ms: number): Us {\n return Math.round(ms * US_PER_MS) as Us\n}\n\n/** 微秒 → 毫秒 */\nexport function usToMs(us: number): Ms {\n return Math.round(us / US_PER_MS) as Ms\n}\n\n/** 数字 → 微秒(用于从 DOM/WebAV 回调拿到的裸数字) */\nexport function asUs(n: number): Us {\n return Math.round(n) as Us\n}\n\n/** 数字 → 像素 */\nexport function asPx(n: number): Px {\n return n as Px\n}\n\n/**\n * 微秒 → 像素。\n * @param us 时间(微秒)\n * @param pxPerSecond 每秒对应多少像素\n */\nexport function usToPx(us: number, pxPerSecond: number): Px {\n return ((us / US_PER_SECOND) * pxPerSecond) as Px\n}\n\n/**\n * 像素 → 微秒。\n * @param px 像素\n * @param pxPerSecond 每秒对应多少像素\n */\nexport function pxToUs(px: number, pxPerSecond: number): Us {\n return Math.round((px / pxPerSecond) * US_PER_SECOND) as Us\n}\n\n/** 微秒 → `HH:MM:SS.mmm` */\nexport function formatTimecode(us: number, showMs = true): string {\n const safe = Math.max(0, us)\n const totalMs = Math.floor(safe / 1000)\n const ms = totalMs % 1000\n const totalSec = Math.floor(totalMs / 1000)\n const sec = totalSec % 60\n const min = Math.floor(totalSec / 60) % 60\n const hour = Math.floor(totalSec / 3600)\n const pad = (n: number, len = 2) => String(n).padStart(len, '0')\n const base = `${pad(hour)}:${pad(min)}:${pad(sec)}`\n return showMs ? `${base}.${pad(ms, 3)}` : base\n}\n\n/** 微秒 → `MM:SS` 或 `HH:MM:SS`,列表展示用 */\nexport function formatDuration(us: number): string {\n if (!Number.isFinite(us)) return '∞'\n const totalSec = Math.max(0, Math.round(us / US_PER_SECOND))\n const sec = totalSec % 60\n const min = Math.floor(totalSec / 60) % 60\n const hour = Math.floor(totalSec / 3600)\n const pad = (n: number) => String(n).padStart(2, '0')\n return hour > 0 ? `${hour}:${pad(min)}:${pad(sec)}` : `${pad(min)}:${pad(sec)}`\n}\n\nexport interface RulerTick {\n us: Us\n px: Px\n major: boolean\n}\n\nconst TICK_STEPS_US = [\n 100_000, 200_000, 500_000, 1_000_000, 2_000_000, 5_000_000, 10_000_000, 15_000_000,\n 30_000_000, 60_000_000, 120_000_000, 300_000_000, 600_000_000,\n]\n\n/**\n * 根据缩放级别生成刻度尺。\n *\n * @param pxPerSecond 每秒像素\n * @param viewportWidthPx 可视宽度(像素)\n * @param scrollLeftPx 横向滚动偏移(像素)\n * @returns 主刻度(带文字)与次刻度\n */\nexport function buildTicks(\n pxPerSecond: number,\n viewportWidthPx: number,\n scrollLeftPx = 0,\n): RulerTick[] {\n const startUs = pxToUs(scrollLeftPx, pxPerSecond)\n const endUs = pxToUs(scrollLeftPx + viewportWidthPx, pxPerSecond)\n const minLabelSpacingPx = 72\n const step =\n TICK_STEPS_US.find((s) => usToPx(s, pxPerSecond) >= minLabelSpacingPx) ??\n TICK_STEPS_US[TICK_STEPS_US.length - 1]\n const minorStep = step / 5\n const first = Math.floor(startUs / step) * step\n const ticks: RulerTick[] = []\n for (let us = first; us <= endUs + step; us += minorStep) {\n if (us < 0) continue\n ticks.push({\n us: us as Us,\n px: usToPx(us, pxPerSecond),\n major: us % step === 0,\n })\n }\n return ticks\n}\n\n/** 保证值落在 [min, max] */\nexport function clamp(value: number, min: number, max: number): number {\n return Math.min(max, Math.max(min, value))\n}\n"],"mappings":"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"}