diff --git a/src/lib/ai/mutations.ts b/src/lib/ai/mutations.ts index 02cf65c..b48a04d 100644 --- a/src/lib/ai/mutations.ts +++ b/src/lib/ai/mutations.ts @@ -34,7 +34,7 @@ import type { RemoveKeyframeInput, RemoveKeyframeOutput } from './schemas'; -import type { ProjectData } from '$lib/schemas/animation'; +import type { Layer, ProjectData } from '$lib/schemas/animation'; /** * Context for resolving layer IDs (e.g. "layer_0" -> "actual-uuid") @@ -122,7 +122,7 @@ export function mutateCreateLayer( try { const layer = createLayer(input.type, { props: input.props, - trasform: input.position, + transform: input.transform, projectDimensions: { width: ctx.project.width, height: ctx.project.height @@ -290,31 +290,18 @@ export function mutateEditLayer(ctx: MutationContext, input: EditLayerInput): Ed const layer = ctx.project.layers[layerIndex]; try { - if (input.updates.name !== undefined) layer.name = input.updates.name; - if (input.updates.visible !== undefined) layer.visible = input.updates.visible; - if (input.updates.locked !== undefined) layer.locked = input.updates.locked; - - if (input.updates.position) { - layer.transform.x = input.updates.position.x ?? layer.transform.x; - layer.transform.y = input.updates.position.y ?? layer.transform.y; - layer.transform.z = input.updates.position.z ?? layer.transform.z; + if (input.updates.name !== undefined) { + layer.name = input.updates.name; } - - if (input.updates.scale) { - layer.transform.scaleX = input.updates.scale.x ?? layer.transform.scaleX; - layer.transform.scaleY = input.updates.scale.y ?? layer.transform.scaleY; + if (input.updates.visible !== undefined) { + layer.visible = input.updates.visible; } - - if (input.updates.rotation !== undefined) { - layer.transform.rotationZ = (input.updates.rotation * Math.PI) / 180; + if (input.updates.locked !== undefined) { + layer.locked = input.updates.locked; } - // Handle 3D rotation (rotationX and rotationY in degrees) - if (input.updates.rotationX !== undefined) { - layer.transform.rotationX = (input.updates.rotationX * Math.PI) / 180; - } - if (input.updates.rotationY !== undefined) { - layer.transform.rotationY = (input.updates.rotationY * Math.PI) / 180; + if (input.updates.transform) { + layer.transform = { ...layer.transform, ...input.updates.transform }; } // Handle anchor point @@ -445,19 +432,14 @@ export function mutateGroupLayers( const groupId = nanoid(); // Create the group layer - const groupLayer = { + const groupLayer: Layer = { id: groupId, name: input.name ?? 'Group', type: 'group' as const, transform: { - x: 0, - y: 0, - z: 0, - rotationX: 0, - rotationY: 0, - rotationZ: 0, - scaleX: 1, - scaleY: 1, + position: { x: 0, y: 0, z: 0 }, + rotation: { x: 0, y: 0, z: 0 }, + scale: { x: 1, y: 1 }, anchor: 'center' as const }, style: { opacity: 1 }, @@ -528,14 +510,14 @@ export function mutateUngroupLayers( for (const layer of ctx.project.layers) { if (layer.parentId === resolvedId) { layer.parentId = undefined; - layer.transform.x += gt.x; - layer.transform.y += gt.y; - layer.transform.z += gt.z; - layer.transform.rotationX += gt.rotationX; - layer.transform.rotationY += gt.rotationY; - layer.transform.rotationZ += gt.rotationZ; - layer.transform.scaleX *= gt.scaleX; - layer.transform.scaleY *= gt.scaleY; + layer.transform.position.x += gt.position.x; + layer.transform.position.y += gt.position.y; + layer.transform.position.z += gt.position.z; + layer.transform.rotation.x += gt.rotation.x; + layer.transform.rotation.y += gt.rotation.y; + layer.transform.rotation.z += gt.rotation.z; + layer.transform.scale.x *= gt.scale.x; + layer.transform.scale.y *= gt.scale.y; layer.style.opacity *= gs.opacity; } } @@ -732,9 +714,9 @@ function applyPresetToProject( let value = kf.value; if (kf.property === 'position.x' && typeof kf.value === 'number') { - value = layer.transform.x + kf.value; + value = layer.transform.position.x + kf.value; } else if (kf.property === 'position.y' && typeof kf.value === 'number') { - value = layer.transform.y + kf.value; + value = layer.transform.position.y + kf.value; } addKeyframeToProject(project, layerId, { diff --git a/src/lib/ai/schemas.ts b/src/lib/ai/schemas.ts index 97f467d..18884c1 100644 --- a/src/lib/ai/schemas.ts +++ b/src/lib/ai/schemas.ts @@ -5,11 +5,11 @@ * Each layer type has its own creation tool (create_text_layer, create_icon_layer, etc.) */ import { z } from 'zod'; -import { InterpolationSchema } from '$lib/schemas/animation'; +import { InterpolationSchema, TransformSchema } from '$lib/schemas/animation'; import { getPresetIds } from '$lib/engine/presets'; import { tool, type InferUITools, type Tool } from 'ai'; import { layerRegistry, getAvailableLayerTypes } from '$lib/layers/registry'; -import { extractDefaultValues } from '$lib/layers/base'; +import { AnchorPointSchema, extractDefaultValues } from '$lib/layers/base'; // ============================================ // Helper Functions @@ -123,7 +123,7 @@ const CreateLayerInputSchema = z name: z.string().optional().describe('Layer name for identification'), visible: z.boolean().optional().default(true).describe('Layer visibility (default: true)'), locked: z.boolean().optional().default(false).describe('Layer locked state (default: false)'), - position: PositionSchema, + transform: TransformSchema, animation: AnimationSchema }) .extend(TimingFieldsSchema.shape) @@ -241,36 +241,8 @@ export const EditLayerInputSchema = z.object({ name: z.string().optional(), visible: z.boolean().optional(), locked: z.boolean().optional(), - position: z - .object({ - x: z.number().optional(), - y: z.number().optional(), - z: z.number().optional().describe('Z position (depth)') - }) - .optional(), - scale: z - .object({ - x: z.number().optional(), - y: z.number().optional() - }) - .optional(), - rotation: z.number().optional().describe('Rotation around Z axis (degrees)'), - rotationX: z.number().optional().describe('Rotation around X axis (degrees)'), - rotationY: z.number().optional().describe('Rotation around Y axis (degrees)'), - anchor: z - .enum([ - 'top-left', - 'top-center', - 'top-right', - 'center-left', - 'center', - 'center-right', - 'bottom-left', - 'bottom-center', - 'bottom-right' - ]) - .optional() - .describe('Anchor point for transformations'), + transform: TransformSchema.optional(), + anchor: AnchorPointSchema.optional().describe('Anchor point for transformations'), opacity: z.number().min(0).max(1).optional(), props: z.record(z.string(), z.unknown()).optional() }) diff --git a/src/lib/ai/system-prompt.ts b/src/lib/ai/system-prompt.ts index ce21e2d..0f8fd24 100644 --- a/src/lib/ai/system-prompt.ts +++ b/src/lib/ai/system-prompt.ts @@ -28,9 +28,9 @@ export function buildSystemPrompt(project: Project): string { ## Transform Capabilities -- **Position**: x, y (horizontal/vertical), z (depth) -- **Scale**: x, y (independent or use matching values for proportional scaling) -- **Rotation**: rotation (Z-axis in degrees), rotationX, rotationY (3D rotation in degrees) +- **Position**: position.x, position.y (horizontal/vertical), position.z (depth) +- **Scale**: scale.x, scale.y (independent or use matching values for proportional scaling) +- **Rotation**: rotation.z (Z-axis in radians), rotation.x, rotation.y (3D rotation in radians) - **Anchor**: Set anchor point (top-left, top-center, top-right, center-left, center, center-right, bottom-left, bottom-center, bottom-right) to control which point of the layer is positioned at the transform coordinates ## Keyframe Management @@ -115,7 +115,7 @@ function buildCanvasState(project: Project): string { const kfList = kfs .sort((a, b) => a.time - b.time) .map( - (kf) => `t=${kf.time}s: ${JSON.stringify(kf.value)} (${kf.interpolation.strategy})` + (kf) => `t=${kf.time}s: ${JSON.stringify(kf.value)} (${kf.interpolation?.strategy})` ) .join(', '); keyframesDetail += `\n ${prop}: [${kfList}]`; @@ -123,7 +123,7 @@ function buildCanvasState(project: Project): string { } return `${index}. "${layer.name}" (id: "${layer.id}", type: ${layer.type}) - pos: (${layer.transform.x}, ${layer.transform.y}) | scale: (${layer.transform.scaleX}, ${layer.transform.scaleY}) | rotation: ${layer.transform.rotationZ} rad | opacity: ${layer.style.opacity} + pos: (${layer.transform.position.x}, ${layer.transform.position.y}) | scale: (${layer.transform.scale.x}, ${layer.transform.scale.y}) | rotation: ${layer.transform.rotation.z} rad | opacity: ${layer.style.opacity} props: {${propsPreview || 'none'}}${keyframesDetail || '\n keyframes: none'}`; }) .join('\n\n'); diff --git a/src/lib/components/ai/ai-chat.svelte b/src/lib/components/ai/ai-chat.svelte index 1b13fb6..6501320 100644 --- a/src/lib/components/ai/ai-chat.svelte +++ b/src/lib/components/ai/ai-chat.svelte @@ -92,7 +92,7 @@ name: input.name, visible: input.visible, locked: input.locked, - position: input.position, + transform: input.transform, props: input.props || {}, animation: input.animation, enterTime: input.enterTime, diff --git a/src/lib/components/editor/keyframe-card.svelte b/src/lib/components/editor/keyframe-card.svelte index fe2c85f..fcf69bc 100644 --- a/src/lib/components/editor/keyframe-card.svelte +++ b/src/lib/components/editor/keyframe-card.svelte @@ -1,73 +1,67 @@
-
+
+ + + /> {/snippet} @@ -236,7 +303,7 @@ min={0} max={projectStore.state.duration} onchange={handleTimeChange} - class="max-w-12 shrink-0" + class="w-12 shrink-0" /> {/if} = - + {formatValue(keyframe.value)}
+ + {#if hasMultipleFamilies} +
+ Family: + handleStrategyChange(e.currentTarget.value)} - class="h-7 flex-1 rounded-md border border-input bg-background px-2 text-xs focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-none" - > - {#each strategyOptions as option (option.value)} - - {/each} - + {#if currentFamilyOptions.length > 0} +