feat: add redo, toggle track mute/visibility, and trim support
- redo: re-applies the last undone action via core.command.redo() - toggle_track_mute: toggles mute on video/audio tracks via ToggleTrackMuteCommand - toggle_track_visibility: toggles visibility on overlay tracks via ToggleTrackVisibilityCommand - update_clip now supports trimStart/trimEnd for slip-trimming clips without moving them on the timeline
This commit is contained in:
parent
28b825267f
commit
f474a87267
|
|
@ -3,6 +3,8 @@ import { TICKS_PER_SECOND } from "@/lib/wasm";
|
|||
import { buildContextFromEditorState } from "@/agent/context-mapper";
|
||||
import { BatchCommand } from "@/lib/commands";
|
||||
import { AddTrackCommand, InsertElementCommand } from "@/lib/commands/timeline";
|
||||
import { ToggleTrackMuteCommand } from "@/lib/commands/timeline/track/toggle-track-mute";
|
||||
import { ToggleTrackVisibilityCommand } from "@/lib/commands/timeline/track/toggle-track-visibility";
|
||||
import { DEFAULT_NEW_ELEMENT_DURATION } from "@/lib/timeline/creation";
|
||||
import {
|
||||
buildElementFromMedia,
|
||||
|
|
@ -89,6 +91,64 @@ export const EditorContextAdapter = {
|
|||
return { remainingUndoDepth };
|
||||
},
|
||||
|
||||
redo(): { remainingRedoDepth: number } | { error: string } {
|
||||
const core = EditorCore.getInstance();
|
||||
if (!core.command.canRedo()) {
|
||||
return { error: "Nothing to redo" };
|
||||
}
|
||||
core.command.redo();
|
||||
const remainingRedoDepth = core.command.canRedo() ? 1 : 0;
|
||||
return { remainingRedoDepth };
|
||||
},
|
||||
|
||||
toggleTrackMute({
|
||||
trackId,
|
||||
}: {
|
||||
trackId: string;
|
||||
}): { trackId: string } | { error: string } {
|
||||
const core = EditorCore.getInstance();
|
||||
const activeScene = core.scenes.getActiveSceneOrNull();
|
||||
if (!activeScene) {
|
||||
return { error: "No active timeline" };
|
||||
}
|
||||
|
||||
const track = findTrackInSceneTracks({
|
||||
tracks: activeScene.tracks,
|
||||
trackId,
|
||||
});
|
||||
if (!track) {
|
||||
return { error: `Track not found: ${trackId}` };
|
||||
}
|
||||
|
||||
core.command.execute({ command: new ToggleTrackMuteCommand(trackId) });
|
||||
return { trackId };
|
||||
},
|
||||
|
||||
toggleTrackVisibility({
|
||||
trackId,
|
||||
}: {
|
||||
trackId: string;
|
||||
}): { trackId: string } | { error: string } {
|
||||
const core = EditorCore.getInstance();
|
||||
const activeScene = core.scenes.getActiveSceneOrNull();
|
||||
if (!activeScene) {
|
||||
return { error: "No active timeline" };
|
||||
}
|
||||
|
||||
const track = findTrackInSceneTracks({
|
||||
tracks: activeScene.tracks,
|
||||
trackId,
|
||||
});
|
||||
if (!track) {
|
||||
return { error: `Track not found: ${trackId}` };
|
||||
}
|
||||
|
||||
core.command.execute({
|
||||
command: new ToggleTrackVisibilityCommand(trackId),
|
||||
});
|
||||
return { trackId };
|
||||
},
|
||||
|
||||
duplicateElements({ elementIds }: { elementIds: string[] }):
|
||||
| {
|
||||
success: boolean;
|
||||
|
|
@ -761,6 +821,8 @@ export const EditorContextAdapter = {
|
|||
elementId,
|
||||
name,
|
||||
mask,
|
||||
trimStart,
|
||||
trimEnd,
|
||||
opacity,
|
||||
positionX,
|
||||
positionY,
|
||||
|
|
@ -779,6 +841,8 @@ export const EditorContextAdapter = {
|
|||
maskType?: string;
|
||||
params?: Record<string, number | string | boolean>;
|
||||
};
|
||||
trimStart?: number;
|
||||
trimEnd?: number;
|
||||
opacity?: number;
|
||||
positionX?: number;
|
||||
positionY?: number;
|
||||
|
|
@ -831,6 +895,29 @@ export const EditorContextAdapter = {
|
|||
applied.muted = muted;
|
||||
}
|
||||
|
||||
if (trimStart !== undefined || trimEnd !== undefined) {
|
||||
if (
|
||||
(trimStart !== undefined && typeof trimStart !== "number") ||
|
||||
(trimEnd !== undefined && typeof trimEnd !== "number")
|
||||
) {
|
||||
return { error: "trimStart and trimEnd must be numbers (seconds)" };
|
||||
}
|
||||
if (trimStart !== undefined && trimStart < 0) {
|
||||
return { error: "trimStart must be >= 0" };
|
||||
}
|
||||
if (trimEnd !== undefined && trimEnd < 0) {
|
||||
return { error: "trimEnd must be >= 0" };
|
||||
}
|
||||
if (trimStart !== undefined) {
|
||||
patch.trimStart = secondsToTicks(trimStart);
|
||||
applied.trimStart = trimStart;
|
||||
}
|
||||
if (trimEnd !== undefined) {
|
||||
patch.trimEnd = secondsToTicks(trimEnd);
|
||||
applied.trimEnd = trimEnd;
|
||||
}
|
||||
}
|
||||
|
||||
if (opacity !== undefined) {
|
||||
if (typeof opacity !== "number" || opacity < 0 || opacity > 100) {
|
||||
return { error: "opacity must be a number between 0 and 100" };
|
||||
|
|
|
|||
|
|
@ -28,3 +28,6 @@ import "@/agent/tools/apply-effect.tool";
|
|||
import "@/agent/tools/update-effect.tool";
|
||||
import "@/agent/tools/update-clip.tool";
|
||||
import "@/agent/tools/undo.tool";
|
||||
import "@/agent/tools/redo.tool";
|
||||
import "@/agent/tools/toggle-track-mute.tool";
|
||||
import "@/agent/tools/toggle-track-visibility.tool";
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
import type { AgentContext, ToolDefinition } from "@/agent/types";
|
||||
import { toolRegistry } from "@/agent/tools/registry";
|
||||
import { redoSchema } from "@/agent/tools/schemas";
|
||||
import { EditorContextAdapter } from "@/agent/context";
|
||||
|
||||
const redoTool: ToolDefinition = {
|
||||
...redoSchema,
|
||||
execute: async (
|
||||
_args: Record<string, unknown>,
|
||||
_context: AgentContext,
|
||||
): Promise<{ remainingRedoDepth: number } | { error: string }> => {
|
||||
return EditorContextAdapter.redo();
|
||||
},
|
||||
};
|
||||
|
||||
toolRegistry.register(redoSchema.name, redoTool);
|
||||
|
|
@ -171,6 +171,27 @@ export const updateEffectSchema: ToolSchema = {
|
|||
],
|
||||
};
|
||||
|
||||
export const redoSchema: ToolSchema = {
|
||||
name: "redo",
|
||||
description:
|
||||
"Redoes the last undone action. Only works after an undo. Returns whether there are more actions to redo.",
|
||||
parameters: [],
|
||||
};
|
||||
|
||||
export const toggleTrackMuteSchema: ToolSchema = {
|
||||
name: "toggle_track_mute",
|
||||
description:
|
||||
"Toggles mute on a timeline track. Use list_timeline to discover trackIds. Only works on tracks that support audio (video and audio tracks). Returns the new muted state.",
|
||||
parameters: [{ key: "trackId", type: "string", required: true }],
|
||||
};
|
||||
|
||||
export const toggleTrackVisibilitySchema: ToolSchema = {
|
||||
name: "toggle_track_visibility",
|
||||
description:
|
||||
"Toggles visibility on a timeline track. Hidden tracks are not rendered in the preview. Use list_timeline to discover trackIds. Returns the new hidden state.",
|
||||
parameters: [{ key: "trackId", type: "string", required: true }],
|
||||
};
|
||||
|
||||
export const undoSchema: ToolSchema = {
|
||||
name: "undo",
|
||||
description:
|
||||
|
|
@ -195,11 +216,13 @@ export const getElementSchema: ToolSchema = {
|
|||
export const updateClipSchema: ToolSchema = {
|
||||
name: "update_clip",
|
||||
description:
|
||||
"Updates properties of any timeline element (video, image, graphic, text, sticker, audio, effect). Use list_timeline to discover elementId, then get_element to inspect current values. Only provide the properties you want to change. mask: { action: 'add', maskType } to add, { action: 'update', params: {...} } to modify, { action: 'remove' } to delete. Mask types: rectangle, ellipse, heart, diamond, star, split, cinematic-bars. Only video/image/graphic support masks. name: rename the element. opacity: 0-100. positionX/positionY: position offset. rotation: degrees. scaleX/scaleY: scale factor. blendMode: normal, darken, multiply, screen, etc. hidden: boolean. volume: 0-100 (video/audio only). muted: boolean (video/audio only).",
|
||||
"Updates properties of any timeline element (video, image, graphic, text, sticker, audio, effect). Use list_timeline to discover elementId, then get_element to inspect current values. Only provide the properties you want to change. mask: { action: 'add', maskType } to add, { action: 'update', params: {...} } to modify, { action: 'remove' } to delete. Mask types: rectangle, ellipse, heart, diamond, star, split, cinematic-bars. Only video/image/graphic support masks. name: rename the element. trimStart/trimEnd: seconds to trim from the source start/end (slip trim without moving the clip). opacity: 0-100. positionX/positionY: position offset. rotation: degrees. scaleX/scaleY: scale factor. blendMode: normal, darken, multiply, screen, etc. hidden: boolean. volume: 0-100 (video/audio only). muted: boolean (video/audio only).",
|
||||
parameters: [
|
||||
{ key: "elementId", type: "string", required: true },
|
||||
{ key: "name", type: "string", required: false },
|
||||
{ key: "mask", type: "object", required: false },
|
||||
{ key: "trimStart", type: "number", required: false },
|
||||
{ key: "trimEnd", type: "number", required: false },
|
||||
{ key: "opacity", type: "number", required: false },
|
||||
{ key: "positionX", type: "number", required: false },
|
||||
{ key: "positionY", type: "number", required: false },
|
||||
|
|
@ -236,4 +259,7 @@ export const providerToolSchemas: ToolSchema[] = [
|
|||
updateEffectSchema,
|
||||
updateClipSchema,
|
||||
undoSchema,
|
||||
redoSchema,
|
||||
toggleTrackMuteSchema,
|
||||
toggleTrackVisibilitySchema,
|
||||
];
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
import type { AgentContext, ToolDefinition } from "@/agent/types";
|
||||
import { toolRegistry } from "@/agent/tools/registry";
|
||||
import { toggleTrackMuteSchema } from "@/agent/tools/schemas";
|
||||
import { EditorContextAdapter } from "@/agent/context";
|
||||
|
||||
const toggleTrackMuteTool: ToolDefinition = {
|
||||
...toggleTrackMuteSchema,
|
||||
execute: async (
|
||||
args: Record<string, unknown>,
|
||||
_context: AgentContext,
|
||||
): Promise<{ trackId: string } | { error: string }> => {
|
||||
const trackId = args.trackId;
|
||||
|
||||
if (typeof trackId !== "string" || !trackId.trim()) {
|
||||
return { error: "Invalid trackId" };
|
||||
}
|
||||
|
||||
return EditorContextAdapter.toggleTrackMute({ trackId });
|
||||
},
|
||||
};
|
||||
|
||||
toolRegistry.register(toggleTrackMuteSchema.name, toggleTrackMuteTool);
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
import type { AgentContext, ToolDefinition } from "@/agent/types";
|
||||
import { toolRegistry } from "@/agent/tools/registry";
|
||||
import { toggleTrackVisibilitySchema } from "@/agent/tools/schemas";
|
||||
import { EditorContextAdapter } from "@/agent/context";
|
||||
|
||||
const toggleTrackVisibilityTool: ToolDefinition = {
|
||||
...toggleTrackVisibilitySchema,
|
||||
execute: async (
|
||||
args: Record<string, unknown>,
|
||||
_context: AgentContext,
|
||||
): Promise<{ trackId: string } | { error: string }> => {
|
||||
const trackId = args.trackId;
|
||||
|
||||
if (typeof trackId !== "string" || !trackId.trim()) {
|
||||
return { error: "Invalid trackId" };
|
||||
}
|
||||
|
||||
return EditorContextAdapter.toggleTrackVisibility({ trackId });
|
||||
},
|
||||
};
|
||||
|
||||
toolRegistry.register(
|
||||
toggleTrackVisibilitySchema.name,
|
||||
toggleTrackVisibilityTool,
|
||||
);
|
||||
|
|
@ -21,6 +21,8 @@ const updateClipTool: ToolDefinition = {
|
|||
params?: Record<string, number | string | boolean>;
|
||||
}
|
||||
| undefined;
|
||||
const trimStart = args.trimStart as number | undefined;
|
||||
const trimEnd = args.trimEnd as number | undefined;
|
||||
const opacity = args.opacity as number | undefined;
|
||||
const positionX = args.positionX as number | undefined;
|
||||
const positionY = args.positionY as number | undefined;
|
||||
|
|
@ -39,6 +41,8 @@ const updateClipTool: ToolDefinition = {
|
|||
const hasUpdate =
|
||||
name !== undefined ||
|
||||
mask !== undefined ||
|
||||
trimStart !== undefined ||
|
||||
trimEnd !== undefined ||
|
||||
opacity !== undefined ||
|
||||
positionX !== undefined ||
|
||||
positionY !== undefined ||
|
||||
|
|
@ -71,6 +75,8 @@ const updateClipTool: ToolDefinition = {
|
|||
elementId,
|
||||
name,
|
||||
mask,
|
||||
trimStart,
|
||||
trimEnd,
|
||||
opacity,
|
||||
positionX,
|
||||
positionY,
|
||||
|
|
|
|||
Loading…
Reference in New Issue