summaryrefslogtreecommitdiff
path: root/apps/web/src/lib/commands
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2026-04-18 01:40:18 +0200
committerMaze Winther <mazewinther@gmail.com>2026-04-18 01:40:18 +0200
commit011f51051c9750706978d6c89ce84dd15560c335 (patch)
tree2e7b23ebebb798934d6cb6aeace855f31699b633 /apps/web/src/lib/commands
parentc46d28891fee62ca2d3132da876af6411a41d9da (diff)
downloadopencut-011f51051c9750706978d6c89ce84dd15560c335.tar.gz
feat: clip volume line and timeline waveform improvements
Diffstat (limited to 'apps/web/src/lib/commands')
-rw-r--r--apps/web/src/lib/commands/media/remove-media-asset.ts8
-rw-r--r--apps/web/src/lib/commands/timeline/element/insert-element.ts1
-rw-r--r--apps/web/src/lib/commands/timeline/element/split-elements.ts122
-rw-r--r--apps/web/src/lib/commands/timeline/element/toggle-source-audio-separation.ts48
-rw-r--r--apps/web/src/lib/commands/timeline/track/add-track.ts5
5 files changed, 101 insertions, 83 deletions
diff --git a/apps/web/src/lib/commands/media/remove-media-asset.ts b/apps/web/src/lib/commands/media/remove-media-asset.ts
index 5601a7e3..85d187d9 100644
--- a/apps/web/src/lib/commands/media/remove-media-asset.ts
+++ b/apps/web/src/lib/commands/media/remove-media-asset.ts
@@ -1,8 +1,10 @@
import { Command, type CommandResult } from "@/lib/commands/base-command";
import { EditorCore } from "@/core";
import type { MediaAsset } from "@/lib/media/types";
+import { buildWaveformSourceKey } from "@/lib/media/waveform-summary";
import { storageService } from "@/services/storage/service";
import { videoCache } from "@/services/video-cache/service";
+import { waveformCache } from "@/services/waveform-cache/service";
import { hasMediaId } from "@/lib/timeline/element-utils";
import type { SceneTracks } from "@/lib/timeline";
@@ -41,6 +43,12 @@ export class RemoveMediaAssetCommand extends Command {
}
videoCache.clearVideo({ mediaId: this.assetId });
+ waveformCache.clearSource({
+ sourceKey: buildWaveformSourceKey({
+ kind: "media",
+ id: this.assetId,
+ }),
+ });
editor.media.setAssets({
assets: assets.filter((media) => media.id !== this.assetId),
diff --git a/apps/web/src/lib/commands/timeline/element/insert-element.ts b/apps/web/src/lib/commands/timeline/element/insert-element.ts
index 0e806d6e..ed05b8c6 100644
--- a/apps/web/src/lib/commands/timeline/element/insert-element.ts
+++ b/apps/web/src/lib/commands/timeline/element/insert-element.ts
@@ -3,7 +3,6 @@ import { EditorCore } from "@/core";
import type {
CreateTimelineElement,
SceneTracks,
- TimelineTrack,
TimelineElement,
TrackType,
} from "@/lib/timeline";
diff --git a/apps/web/src/lib/commands/timeline/element/split-elements.ts b/apps/web/src/lib/commands/timeline/element/split-elements.ts
index 9799f7e8..0f2495f3 100644
--- a/apps/web/src/lib/commands/timeline/element/split-elements.ts
+++ b/apps/web/src/lib/commands/timeline/element/split-elements.ts
@@ -37,7 +37,9 @@ export class SplitElementsCommand extends Command {
this.savedState = editor.scenes.getActiveScene().tracks;
this.rightSideElements = [];
- const splitTrack = <TTrack extends { id: string; elements: TimelineElement[] }>(
+ const splitTrack = <
+ TTrack extends { id: string; elements: TimelineElement[] },
+ >(
track: TTrack,
): TTrack => {
const elementsToSplit = this.elements.filter(
@@ -48,7 +50,7 @@ export class SplitElementsCommand extends Command {
return track;
}
- let elements = track.elements.flatMap((element) => {
+ const elements = track.elements.flatMap((element) => {
const shouldSplit = elementsToSplit.some(
(target) => target.elementId === element.id,
);
@@ -87,67 +89,67 @@ export class SplitElementsCommand extends Command {
splitTime: relativeTime,
shouldIncludeSplitBoundary: true,
});
+ let splitResult: TimelineElement[];
if (this.retainSide === "left") {
- return [
- {
- ...element,
- duration: leftVisibleDuration,
- trimEnd: element.trimEnd + rightSourceSpan,
- name: `${element.name} (left)`,
- animations: leftAnimations,
- ...(retimeRef !== undefined ? { retime: retimeRef } : {}),
- },
- ];
- }
-
- if (this.retainSide === "right") {
- const newId = generateUUID();
- this.rightSideElements.push({
- trackId: track.id,
- elementId: newId,
- });
- return [
- {
- ...element,
- id: newId,
- startTime: this.splitTime,
- duration: rightVisibleDuration,
- trimStart: element.trimStart + leftSourceSpan,
- name: `${element.name} (right)`,
- animations: rightAnimations,
- ...(retimeRef !== undefined ? { retime: retimeRef } : {}),
- },
- ];
- }
-
- // "both" - split into two pieces
- const secondElementId = generateUUID();
- this.rightSideElements.push({
- trackId: track.id,
- elementId: secondElementId,
- });
+ splitResult = [
+ {
+ ...element,
+ duration: leftVisibleDuration,
+ trimEnd: element.trimEnd + rightSourceSpan,
+ name: `${element.name} (left)`,
+ animations: leftAnimations,
+ ...(retimeRef !== undefined ? { retime: retimeRef } : {}),
+ },
+ ];
+ } else if (this.retainSide === "right") {
+ const newId = generateUUID();
+ this.rightSideElements.push({
+ trackId: track.id,
+ elementId: newId,
+ });
+ splitResult = [
+ {
+ ...element,
+ id: newId,
+ startTime: this.splitTime,
+ duration: rightVisibleDuration,
+ trimStart: element.trimStart + leftSourceSpan,
+ name: `${element.name} (right)`,
+ animations: rightAnimations,
+ ...(retimeRef !== undefined ? { retime: retimeRef } : {}),
+ },
+ ];
+ } else {
+ // "both" - split into two pieces
+ const secondElementId = generateUUID();
+ this.rightSideElements.push({
+ trackId: track.id,
+ elementId: secondElementId,
+ });
+ splitResult = [
+ {
+ ...element,
+ duration: leftVisibleDuration,
+ trimEnd: element.trimEnd + rightSourceSpan,
+ name: `${element.name} (left)`,
+ animations: leftAnimations,
+ ...(retimeRef !== undefined ? { retime: retimeRef } : {}),
+ },
+ {
+ ...element,
+ id: secondElementId,
+ startTime: this.splitTime,
+ duration: rightVisibleDuration,
+ trimStart: element.trimStart + leftSourceSpan,
+ name: `${element.name} (right)`,
+ animations: rightAnimations,
+ ...(retimeRef !== undefined ? { retime: retimeRef } : {}),
+ },
+ ];
+ }
- return [
- {
- ...element,
- duration: leftVisibleDuration,
- trimEnd: element.trimEnd + rightSourceSpan,
- name: `${element.name} (left)`,
- animations: leftAnimations,
- ...(retimeRef !== undefined ? { retime: retimeRef } : {}),
- },
- {
- ...element,
- id: secondElementId,
- startTime: this.splitTime,
- duration: rightVisibleDuration,
- trimStart: element.trimStart + leftSourceSpan,
- name: `${element.name} (right)`,
- animations: rightAnimations,
- ...(retimeRef !== undefined ? { retime: retimeRef } : {}),
- },
- ];
+ return splitResult;
});
return { ...track, elements } as TTrack;
diff --git a/apps/web/src/lib/commands/timeline/element/toggle-source-audio-separation.ts b/apps/web/src/lib/commands/timeline/element/toggle-source-audio-separation.ts
index 36b3b7a7..7300f540 100644
--- a/apps/web/src/lib/commands/timeline/element/toggle-source-audio-separation.ts
+++ b/apps/web/src/lib/commands/timeline/element/toggle-source-audio-separation.ts
@@ -5,10 +5,12 @@ import {
canExtractSourceAudio,
isSourceAudioSeparated,
} from "@/lib/timeline/audio-separation";
-import { buildEmptyTrack } from "@/lib/timeline/placement";
+import {
+ applyPlacement,
+ resolveTrackPlacement,
+} from "@/lib/timeline/placement";
import { updateElementInSceneTracks } from "@/lib/timeline/track-element-update";
import type {
- AudioTrack,
SceneTracks,
TimelineElement,
VideoElement,
@@ -35,9 +37,7 @@ export class ToggleSourceAudioSeparationCommand extends Command {
...this.savedState.overlay,
this.savedState.main,
...this.savedState.audio,
- ].find(
- (track) => track.id === this.params.trackId,
- );
+ ].find((track) => track.id === this.params.trackId);
if (!sourceTrack) {
return;
}
@@ -61,8 +61,7 @@ export class ToggleSourceAudioSeparationCommand extends Command {
return;
}
- const mediaAsset = editor
- .media
+ const mediaAsset = editor.media
.getAssets()
.find((asset) => asset.id === videoElement.mediaId);
if (!canExtractSourceAudio(videoElement, mediaAsset)) {
@@ -78,20 +77,32 @@ export class ToggleSourceAudioSeparationCommand extends Command {
}),
id: generateUUID(),
};
- const newAudioTrack = {
- ...buildEmptyTrack({
- id: generateUUID(),
- type: "audio",
- }),
+ const placementResult = resolveTrackPlacement({
+ tracks: this.savedState,
+ trackType: "audio",
+ timeSpans: [
+ {
+ startTime: separatedAudioElement.startTime,
+ duration: separatedAudioElement.duration,
+ },
+ ],
+ strategy: { type: "firstAvailable" },
+ });
+ if (!placementResult) {
+ return;
+ }
+ const appliedPlacement = applyPlacement({
+ tracks: this.savedState,
+ placementResult,
elements: [separatedAudioElement],
- } as AudioTrack;
+ });
+ if (!appliedPlacement) {
+ return;
+ }
editor.timeline.updateTracks(
updateSourceAudioEnabled({
- tracks: {
- ...this.savedState,
- audio: [...this.savedState.audio, newAudioTrack],
- },
+ tracks: appliedPlacement.updatedTracks,
trackId: this.params.trackId,
elementId: this.params.elementId,
isSourceAudioEnabled: false,
@@ -124,7 +135,8 @@ function updateSourceAudioEnabled({
tracks,
trackId,
elementId,
- elementPredicate: (element): element is VideoElement => element.type === "video",
+ elementPredicate: (element): element is VideoElement =>
+ element.type === "video",
update: (element) => ({
...element,
isSourceAudioEnabled,
diff --git a/apps/web/src/lib/commands/timeline/track/add-track.ts b/apps/web/src/lib/commands/timeline/track/add-track.ts
index 66b9c348..a577c693 100644
--- a/apps/web/src/lib/commands/timeline/track/add-track.ts
+++ b/apps/web/src/lib/commands/timeline/track/add-track.ts
@@ -69,10 +69,7 @@ function buildAudioTrackState({
insertIndex: number;
trackId: string;
}): SceneTracks {
- const audioInsertIndex = Math.max(
- 0,
- insertIndex - tracks.overlay.length - 1,
- );
+ const audioInsertIndex = Math.max(0, insertIndex - tracks.overlay.length - 1);
const newTrack = buildEmptyTrack({
id: trackId,
type: "audio",