diff --git a/src/components/video-editor/projectPersistence.ts b/src/components/video-editor/projectPersistence.ts index c5d25385..835bb24c 100644 --- a/src/components/video-editor/projectPersistence.ts +++ b/src/components/video-editor/projectPersistence.ts @@ -47,6 +47,7 @@ import { getDefaultCaptionFontFamily, type SpeedRegion, type TrimRegion, + type ClipRegion, type WebcamOverlaySettings, type ZoomRegion, type ZoomTransitionEasing, @@ -82,6 +83,7 @@ export interface ProjectEditorState { cropRegion: CropRegion; zoomRegions: ZoomRegion[]; trimRegions: TrimRegion[]; + clipRegions: ClipRegion[]; speedRegions: SpeedRegion[]; annotationRegions: AnnotationRegion[]; audioRegions: AudioRegion[]; @@ -292,6 +294,23 @@ export function normalizeProjectEditor(editor: Partial): Pro }) : []; + const normalizedClipRegions: ClipRegion[] = Array.isArray((editor as any).clipRegions) + ? ((editor as any).clipRegions as ClipRegion[]) + .filter((region): region is ClipRegion => Boolean(region && typeof region.id === "string")) + .map((region) => { + const rawStart = isFiniteNumber(region.startMs) ? Math.round(region.startMs) : 0; + const rawEnd = isFiniteNumber(region.endMs) ? Math.round(region.endMs) : rawStart + 1000; + const startMs = Math.max(0, Math.min(rawStart, rawEnd)); + const endMs = Math.max(startMs + 1, rawEnd); + return { + id: region.id, + startMs, + endMs, + speed: isFiniteNumber((region as any).speed) ? (region as any).speed : 1, + }; + }) + : []; + const normalizedSpeedRegions: SpeedRegion[] = Array.isArray(editor.speedRegions) ? editor.speedRegions .filter((region): region is SpeedRegion => Boolean(region && typeof region.id === "string")) @@ -590,6 +609,7 @@ export function normalizeProjectEditor(editor: Partial): Pro }, zoomRegions: normalizedZoomRegions, trimRegions: normalizedTrimRegions, + clipRegions: normalizedClipRegions, speedRegions: normalizedSpeedRegions, annotationRegions: normalizedAnnotationRegions, audioRegions: normalizedAudioRegions, diff --git a/src/components/video-editor/types.ts b/src/components/video-editor/types.ts index 5406210c..730c3f12 100644 --- a/src/components/video-editor/types.ts +++ b/src/components/video-editor/types.ts @@ -127,6 +127,51 @@ export interface TrimRegion { endMs: number; } +export interface ClipRegion { + id: string; + startMs: number; + endMs: number; + speed: number; +} + +/** Convert clip regions (kept segments) to trim regions (gaps to remove). */ +export function clipsToTrims(clips: ClipRegion[], totalDurationMs: number): TrimRegion[] { + if (clips.length === 0) return []; + const sorted = [...clips].sort((a, b) => a.startMs - b.startMs); + const trims: TrimRegion[] = []; + let cursor = 0; + let trimId = 1; + for (const clip of sorted) { + if (clip.startMs > cursor) { + trims.push({ id: `trim-gap-${trimId++}`, startMs: cursor, endMs: clip.startMs }); + } + cursor = clip.endMs; + } + if (cursor < totalDurationMs) { + trims.push({ id: `trim-gap-${trimId++}`, startMs: cursor, endMs: totalDurationMs }); + } + return trims; +} + +/** Convert legacy trim regions to clip regions (complement). */ +export function trimsToClips(trims: TrimRegion[], totalDurationMs: number): ClipRegion[] { + if (trims.length === 0) return [{ id: "clip-1", startMs: 0, endMs: totalDurationMs, speed: 1 }]; + const sorted = [...trims].sort((a, b) => a.startMs - b.startMs); + const clips: ClipRegion[] = []; + let cursor = 0; + let clipId = 1; + for (const trim of sorted) { + if (trim.startMs > cursor) { + clips.push({ id: `clip-${clipId++}`, startMs: cursor, endMs: trim.startMs, speed: 1 }); + } + cursor = trim.endMs; + } + if (cursor < totalDurationMs) { + clips.push({ id: `clip-${clipId++}`, startMs: cursor, endMs: totalDurationMs, speed: 1 }); + } + return clips; +} + export type AnnotationType = "text" | "image" | "figure"; export type ArrowDirection = diff --git a/src/lib/shortcuts.ts b/src/lib/shortcuts.ts index 0a82fe2f..418e9e25 100644 --- a/src/lib/shortcuts.ts +++ b/src/lib/shortcuts.ts @@ -1,6 +1,7 @@ export const SHORTCUT_ACTIONS = [ 'addZoom', 'addTrim', + 'splitClip', 'addSpeed', 'addAnnotation', 'addKeyframe', @@ -68,6 +69,7 @@ export function findConflict( export const DEFAULT_SHORTCUTS: ShortcutsConfig = { addZoom: { key: 'z' }, addTrim: { key: 't' }, + splitClip: { key: 'c' }, addSpeed: { key: 's' }, addAnnotation: { key: 'a' }, addKeyframe: { key: 'f' }, @@ -78,6 +80,7 @@ export const DEFAULT_SHORTCUTS: ShortcutsConfig = { export const SHORTCUT_LABELS: Record = { addZoom: 'Add Zoom', addTrim: 'Add Trim', + splitClip: 'Split Clip', addSpeed: 'Add Speed', addAnnotation: 'Add Annotation', addKeyframe: 'Add Keyframe',