Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 30 additions & 14 deletions src/lib/ai/ai-operations.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* Handles progressive tool execution with layer ID tracking across tool calls.
* Refactored to use shared 'mutations.ts' logic.
*/
import { projectStore } from '$lib/stores/project.svelte';
import type { ProjectStore } from '$lib/stores/project.svelte';
import type {
CreateLayerInput,
CreateLayerOutput,
Expand Down Expand Up @@ -48,9 +48,9 @@ export function resetLayerTracking() {
// Context Helper
// ============================================

function getContext(): MutationContext {
function getContext(projectStore: ProjectStore): MutationContext {
return {
project: projectStore.project,
project: projectStore.state,
layerIdMap: layerIdMap,
layerCreationIndex: layerCreationIndex
};
Expand All @@ -63,8 +63,11 @@ function getContext(): MutationContext {
/**
* Execute create_layer tool
*/
export function executeCreateLayer(input: CreateLayerInput): CreateLayerOutput {
const ctx = getContext();
export function executeCreateLayer(
projectStore: ProjectStore,
input: CreateLayerInput
): CreateLayerOutput {
const ctx = getContext(projectStore);
const result = mutateCreateLayer(ctx, input);

// Update local index tracker
Expand All @@ -86,26 +89,36 @@ export function executeCreateLayer(input: CreateLayerInput): CreateLayerOutput {
/**
* Execute animate_layer tool
*/
export function executeAnimateLayer(input: AnimateLayerInput): AnimateLayerOutput {
return mutateAnimateLayer(getContext(), input);
export function executeAnimateLayer(
projectStore: ProjectStore,
input: AnimateLayerInput
): AnimateLayerOutput {
return mutateAnimateLayer(getContext(projectStore), input);
}

/**
* Execute edit_layer tool
*/
export function executeEditLayer(input: EditLayerInput): EditLayerOutput {
return mutateEditLayer(getContext(), input);
export function executeEditLayer(
projectStore: ProjectStore,
input: EditLayerInput
): EditLayerOutput {
return mutateEditLayer(getContext(projectStore), input);
}

/**
* Execute remove_layer tool
*/
export function executeRemoveLayer(input: RemoveLayerInput): RemoveLayerOutput {
const result = mutateRemoveLayer(getContext(), input);
export function executeRemoveLayer(
projectStore: ProjectStore,
input: RemoveLayerInput
): RemoveLayerOutput {
const result = mutateRemoveLayer(getContext(projectStore), input);
if (result.success) {
if (
projectStore.selectedLayerId &&
getContext().project.layers.find((l) => l.id === projectStore.selectedLayerId) === undefined
getContext(projectStore).project.layers.find((l) => l.id === projectStore.selectedLayerId) ===
undefined
) {
projectStore.selectedLayerId = null;
}
Expand All @@ -116,6 +129,9 @@ export function executeRemoveLayer(input: RemoveLayerInput): RemoveLayerOutput {
/**
* Execute configure_project tool
*/
export function executeConfigureProject(input: ConfigureProjectInput): ConfigureProjectOutput {
return mutateConfigureProject(getContext(), input);
export function executeConfigureProject(
projectStore: ProjectStore,
input: ConfigureProjectInput
): ConfigureProjectOutput {
return mutateConfigureProject(getContext(projectStore), input);
}
17 changes: 10 additions & 7 deletions src/lib/components/ai/ai-chat.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script lang="ts">
import { projectStore } from '$lib/stores/project.svelte';
import { getEditorState } from '$lib/contexts/editor.svelte';
import { Button } from '$lib/components/ui/button';
import { Bot, Loader2, User } from '@lucide/svelte';
import { AI_MODELS, DEFAULT_MODEL_ID } from '$lib/ai/models';
Expand Down Expand Up @@ -32,6 +32,9 @@
import { PersistedState } from 'runed';
import ToolPart from './tool-part.svelte';

const editorState = $derived(getEditorState());
const projectStore = $derived(editorState.project);

interface Props {
selectedModelId?: string;
}
Expand All @@ -48,7 +51,7 @@
api: resolve('/(app)/chat'),
get body() {
return {
project: projectStore.project,
project: projectStore.state,
modelId: selectedModelId
} satisfies Omit<GenerateRequest, 'messages'>;
}
Expand All @@ -68,7 +71,7 @@
const layerType = getLayerTypeFromToolName(toolName);
if (layerType) {
const input = toolCall.input as CreateLayerInput;
result = executeCreateLayer({
result = executeCreateLayer(projectStore, {
type: layerType,
name: input.name,
position: input.position,
Expand All @@ -82,16 +85,16 @@
// Handle other tools
switch (toolName) {
case 'animate_layer':
result = executeAnimateLayer(toolCall.input as AnimateLayerInput);
result = executeAnimateLayer(projectStore, toolCall.input as AnimateLayerInput);
break;
case 'edit_layer':
result = executeEditLayer(toolCall.input as EditLayerInput);
result = executeEditLayer(projectStore, toolCall.input as EditLayerInput);
break;
case 'remove_layer':
result = executeRemoveLayer(toolCall.input as RemoveLayerInput);
result = executeRemoveLayer(projectStore, toolCall.input as RemoveLayerInput);
break;
case 'configure_project':
result = executeConfigureProject(toolCall.input as ConfigureProjectInput);
result = executeConfigureProject(projectStore, toolCall.input as ConfigureProjectInput);
break;
default:
result = { success: false, error: `Unknown tool: ${toolName}` };
Expand Down
18 changes: 13 additions & 5 deletions src/lib/components/editor/VideoRecorder.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,9 @@
<!-- Recording indicator overlay -->
<div class="absolute top-2 left-2 flex items-center gap-2 rounded bg-black/50 px-2 py-1">
<div class="h-2 w-2 animate-pulse rounded-full bg-destructive"></div>
<span class="text-xs font-medium text-white">Recording {recordingMode === 'screen' ? 'Screen' : 'Camera'}</span>
<span class="text-xs font-medium text-white"
>Recording {recordingMode === 'screen' ? 'Screen' : 'Camera'}</span
>
</div>

<!-- Duration overlay -->
Expand Down Expand Up @@ -291,16 +293,22 @@
<div class="flex gap-1 rounded border bg-muted/30 p-1">
<button
type="button"
class="flex flex-1 items-center justify-center gap-1 rounded px-2 py-1.5 text-xs transition-colors {recordingMode === 'camera' ? 'bg-background shadow-sm' : 'hover:bg-background/50'}"
onclick={() => recordingMode = 'camera'}
class="flex flex-1 items-center justify-center gap-1 rounded px-2 py-1.5 text-xs transition-colors {recordingMode ===
'camera'
? 'bg-background shadow-sm'
: 'hover:bg-background/50'}"
onclick={() => (recordingMode = 'camera')}
>
<Camera class="size-3" />
Camera
</button>
<button
type="button"
class="flex flex-1 items-center justify-center gap-1 rounded px-2 py-1.5 text-xs transition-colors {recordingMode === 'screen' ? 'bg-background shadow-sm' : 'hover:bg-background/50'}"
onclick={() => recordingMode = 'screen'}
class="flex flex-1 items-center justify-center gap-1 rounded px-2 py-1.5 text-xs transition-colors {recordingMode ===
'screen'
? 'bg-background shadow-sm'
: 'hover:bg-background/50'}"
onclick={() => (recordingMode = 'screen')}
>
<Monitor class="size-3" />
Screen
Expand Down
5 changes: 4 additions & 1 deletion src/lib/components/editor/canvas/canvas-controls.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button';
import { ZoomIn, ZoomOut, Maximize } from '@lucide/svelte';
import { projectStore } from '$lib/stores/project.svelte';
import { getEditorState } from '$lib/contexts/editor.svelte';

const editorState = $derived(getEditorState());
const projectStore = $derived(editorState.project);

function zoomIn() {
projectStore.setZoom(projectStore.viewport.zoom * 1.25);
Expand Down
37 changes: 20 additions & 17 deletions src/lib/components/editor/canvas/canvas.svelte
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
<script lang="ts">
import { onMount } from 'svelte';
import { projectStore } from '$lib/stores/project.svelte';
import { getEditorState } from '$lib/contexts/editor.svelte';
import CanvasControls from './canvas-controls.svelte';
import PlaybackControls from './playback-controls.svelte';
import LayersRenderer from './layers-renderer.svelte';
import Watermark from './watermark.svelte';
import { getBackgroundColor, getBackgroundImage } from '$lib/schemas/background';

const editorState = $derived(getEditorState());
const projectStore = $derived(editorState.project);

let canvasContainer: HTMLDivElement | undefined = $state();

let {
Expand Down Expand Up @@ -64,10 +67,10 @@

const newTime = projectStore.currentTime + deltaTime;

if (newTime >= projectStore.project.duration) {
if (newTime >= projectStore.state.duration) {
// When recording, stop at the end instead of looping
if (projectStore.isRecording) {
projectStore.setCurrentTime(projectStore.project.duration);
projectStore.setCurrentTime(projectStore.state.duration);
projectStore.pause();
} else {
projectStore.setCurrentTime(0);
Expand Down Expand Up @@ -137,13 +140,13 @@
// Calculate scale factor to fit project dimensions in viewport
// This maintains aspect ratio and leaves some padding
const VIEWPORT_PADDING = 0.9; // Use 90% of available space for padding
const projectAspectRatio = $derived(projectStore.project.width / projectStore.project.height);
const projectAspectRatio = $derived(projectStore.state.width / projectStore.state.height);
const containerAspectRatio = $derived(containerWidth / containerHeight);

const fitScale = $derived(
containerAspectRatio > projectAspectRatio
? (containerHeight * VIEWPORT_PADDING) / projectStore.project.height
: (containerWidth * VIEWPORT_PADDING) / projectStore.project.width
? (containerHeight * VIEWPORT_PADDING) / projectStore.state.height
: (containerWidth * VIEWPORT_PADDING) / projectStore.state.width
);

// Calculate viewport transform for pan, zoom, and fit scale
Expand All @@ -154,8 +157,8 @@
// Calculate scale for recording mode to fit viewport while maintaining aspect ratio
const recordingScale = $derived.by(() => {
if (typeof window === 'undefined') return 1;
const scaleX = window.innerWidth / projectStore.project.width;
const scaleY = window.innerHeight / projectStore.project.height;
const scaleX = window.innerWidth / projectStore.state.width;
const scaleY = window.innerHeight / projectStore.state.height;
return Math.min(1, scaleX, scaleY); // Never scale up, only down
});
</script>
Expand Down Expand Up @@ -187,7 +190,7 @@
style:left="-10000px"
style:top="-10000px"
style:background-color="rgba(127, 127, 127, 0.6)"
style:clip-path={`polygon(evenodd, 0 0, 0 20000px, 20000px 20000px, 20000px 0, 0 0, ${10000 - projectStore.project.width / 2}px ${10000 - projectStore.project.height / 2}px, ${10000 - projectStore.project.width / 2}px ${10000 + projectStore.project.height / 2}px, ${10000 + projectStore.project.width / 2}px ${10000 + projectStore.project.height / 2}px, ${10000 + projectStore.project.width / 2}px ${10000 - projectStore.project.height / 2}px, ${10000 - projectStore.project.width / 2}px ${10000 - projectStore.project.height / 2}px)`}
style:clip-path={`polygon(evenodd, 0 0, 0 20000px, 20000px 20000px, 20000px 0, 0 0, ${10000 - projectStore.state.width / 2}px ${10000 - projectStore.state.height / 2}px, ${10000 - projectStore.state.width / 2}px ${10000 + projectStore.state.height / 2}px, ${10000 + projectStore.state.width / 2}px ${10000 + projectStore.state.height / 2}px, ${10000 + projectStore.state.width / 2}px ${10000 - projectStore.state.height / 2}px, ${10000 - projectStore.state.width / 2}px ${10000 - projectStore.state.height / 2}px)`}
style:pointer-events="none"
></div>
{/if}
Expand All @@ -197,17 +200,17 @@
bind:this={projectViewport}
class="project-viewport"
class:project-viewport-recording={isRecording}
style:width="{projectStore.project.width}px"
style:height="{projectStore.project.height}px"
style:left={isRecording ? 'auto' : `-${projectStore.project.width / 2}px`}
style:top={isRecording ? 'auto' : `-${projectStore.project.height / 2}px`}
style:width="{projectStore.state.width}px"
style:height="{projectStore.state.height}px"
style:left={isRecording ? 'auto' : `-${projectStore.state.width / 2}px`}
style:top={isRecording ? 'auto' : `-${projectStore.state.height / 2}px`}
style:transform={isRecording ? `scale(${recordingScale})` : undefined}
style:perspective="1000px"
style:perspective-origin="center center"
style:transform-style="preserve-3d"
style:isolation="isolate"
style:background-color={getBackgroundColor(projectStore.project.background)}
style:background-image={getBackgroundImage(projectStore.project.background)}
style:background-color={getBackgroundColor(projectStore.state.background)}
style:background-image={getBackgroundImage(projectStore.state.background)}
style:cursor={projectStore.isRecording ? 'none' : undefined}
>
<!-- Layers -->
Expand All @@ -217,9 +220,9 @@
style:pointer-events={projectStore.isRecording ? 'none' : undefined}
>
<LayersRenderer
layers={projectStore.project.layers}
layers={projectStore.state.layers}
currentTime={projectStore.currentTime}
duration={projectStore.project.duration}
duration={projectStore.state.duration}
isPlaying={projectStore.isPlaying}
selectedLayerId={projectStore.selectedLayerId}
disableSelection={projectStore.isRecording}
Expand Down
5 changes: 4 additions & 1 deletion src/lib/components/editor/canvas/playback-controls.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button';
import { Play, Pause, SkipBack } from '@lucide/svelte';
import { projectStore } from '$lib/stores/project.svelte';
import { getEditorState } from '$lib/contexts/editor.svelte';

const editorState = $derived(getEditorState());
const projectStore = $derived(editorState.project);

function togglePlayback() {
if (projectStore.isPlaying) {
Expand Down
13 changes: 8 additions & 5 deletions src/lib/components/editor/editor-layout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,17 @@
import Panel from './panels/panel.svelte';
import KeyboardHandler from './keyboard-handler.svelte';
import { ResizableHandle, ResizablePane, ResizablePaneGroup } from '$lib/components/ui/resizable';
import { projectStore } from '$lib/stores/project.svelte';
import { getEditorState } from '$lib/contexts/editor.svelte';
import { Layers, Settings, Clock, Sparkles } from '@lucide/svelte';
import AiChat from '$lib/components/ai/ai-chat.svelte';
import ModelSelector from '$lib/components/ai/model-selector.svelte';
import { DEFAULT_MODEL_ID } from '$lib/ai/models';
import AddLayer from './panels/add-layer.svelte';
import { IsMobile } from '$lib/hooks/is-mobile.svelte';

const editorState = $derived(getEditorState());
const projectStore = $derived(editorState.project);

interface Props {
projectId?: string | null;
isOwner?: boolean;
Expand Down Expand Up @@ -102,7 +105,7 @@

<!-- Layers Panel -->
<Panel
title="Layers ({projectStore.project.layers.length})"
title="Layers ({projectStore.state.layers.length})"
icon={Layers}
actionsComponent={AddLayer}
collapsible={true}
Expand Down Expand Up @@ -132,7 +135,7 @@
{/snippet}
{#snippet actionsSnippet()}
<span class="text-xs text-muted-foreground">
{projectStore.currentTime.toFixed(2)}s / {projectStore.project.duration}s
{projectStore.currentTime.toFixed(2)}s / {projectStore.state.duration}s
</span>
{/snippet}
</Panel>
Expand Down Expand Up @@ -164,7 +167,7 @@
<ResizablePaneGroup direction="vertical">
<ResizablePane defaultSize={60} minSize={30}>
<Panel
title="Layers ({projectStore.project.layers.length})"
title="Layers ({projectStore.state.layers.length})"
actionsComponent={AddLayer}
>
{#snippet content()}
Expand Down Expand Up @@ -208,7 +211,7 @@
{/snippet}
{#snippet actionsSnippet()}
<span class="text-xs text-muted-foreground">
{projectStore.currentTime.toFixed(2)}s / {projectStore.project.duration}s
{projectStore.currentTime.toFixed(2)}s / {projectStore.state.duration}s
</span>
{/snippet}
</Panel>
Expand Down
Loading