import { create } from 'zustand'; import { Track, Keyframe } from '../utils/timeline'; export type Mode = 'model' | 'render' | 'animate'; export type SkyboxType = 'none' | 'gradient' | 'uploaded'; export type GizmoMode = 'translate' | 'rotate' | 'scale'; export interface AnimationClip { name: string; duration: number; index: number; } export interface SceneObject { id: string; name: string; url: string; file: File; position: [number, number, number]; rotation: [number, number, number]; scale: [number, number, number]; color: string; metalness: number; roughness: number; textureUrl?: string; envMapIntensity: number; animations: AnimationClip[]; selectedAnimIndex: number; animPlaying: boolean; animSpeed: number; animLoop: boolean; } export interface ErrorLog { id: string; message: string; timestamp: number; resolved?: string; resolving?: boolean; } interface StudioStore { // ── Mode ────────────────────────────────────────────────────── mode: Mode; setMode: (mode: Mode) => void; // ── Scene objects ───────────────────────────────────────────── objects: SceneObject[]; selectedId: string | null; addObject: (obj: SceneObject) => void; removeObject: (id: string) => void; updateObject: (id: string, patch: Partial) => void; setSelectedId: (id: string | null) => void; // ── Gizmo ───────────────────────────────────────────────────── gizmoMode: GizmoMode; setGizmoMode: (m: GizmoMode) => void; // ── Timeline / keyframes ────────────────────────────────────── tracks: Track[]; timelineDuration: number; playhead: number; timelinePlaying: boolean; setPlayhead: (t: number) => void; setTimelinePlaying: (v: boolean) => void; setTimelineDuration: (d: number) => void; addKeyframe: (objectId: string, kf: Keyframe) => void; removeKeyframe: (objectId: string, kfId: string) => void; getTrack: (objectId: string) => Track | undefined; // ── Skybox ──────────────────────────────────────────────────── skyboxType: SkyboxType; skyboxUrl: string | null; setSkybox: (type: SkyboxType, url?: string) => void; // ── Lighting / scene ────────────────────────────────────────── ambientIntensity: number; directionalIntensity: number; bgColor: string; showGrid: boolean; showAxes: boolean; postProcessing: boolean; bloomIntensity: number; setAmbientIntensity: (v: number) => void; setDirectionalIntensity: (v: number) => void; setBgColor: (c: string) => void; setShowGrid: (v: boolean) => void; setShowAxes: (v: boolean) => void; setPostProcessing: (v: boolean) => void; setBloomIntensity: (v: number) => void; // ── AI Error Resolver ───────────────────────────────────────── openrouterKey: string; setOpenrouterKey: (k: string) => void; errorLogs: ErrorLog[]; addError: (msg: string) => void; resolveError: (id: string, resolution: string) => void; setErrorResolving: (id: string, v: boolean) => void; clearErrors: () => void; // ── Recording ───────────────────────────────────────────────── isRecording: boolean; setIsRecording: (v: boolean) => void; } export const useStudioStore = create((set, get) => ({ // ── Mode ────────────────────────────────────────────────────── mode: 'model', setMode: (mode) => set({ mode }), // ── Scene objects ───────────────────────────────────────────── objects: [], selectedId: null, addObject: (obj) => set((s) => ({ objects: [...s.objects, obj], selectedId: obj.id })), removeObject: (id) => set((s) => ({ objects: s.objects.filter(o => o.id !== id), selectedId: s.selectedId === id ? null : s.selectedId, tracks: s.tracks.filter(t => t.objectId !== id), })), updateObject: (id, patch) => set((s) => ({ objects: s.objects.map(o => o.id === id ? { ...o, ...patch } : o), })), setSelectedId: (id) => set({ selectedId: id }), // ── Gizmo ───────────────────────────────────────────────────── gizmoMode: 'translate', setGizmoMode: (m) => set({ gizmoMode: m }), // ── Timeline ────────────────────────────────────────────────── tracks: [], timelineDuration: 5, playhead: 0, timelinePlaying: false, setPlayhead: (t) => set({ playhead: t }), setTimelinePlaying: (v) => set({ timelinePlaying: v }), setTimelineDuration:(d) => set({ timelineDuration: d }), addKeyframe: (objectId, kf) => set((s) => { const exists = s.tracks.find(t => t.objectId === objectId); if (exists) { return { tracks: s.tracks.map(t => t.objectId === objectId ? { ...t, keyframes: [...t.keyframes.filter(k => Math.abs(k.time - kf.time) > 0.05), kf] } : t ), }; } return { tracks: [...s.tracks, { objectId, keyframes: [kf] }] }; }), removeKeyframe: (objectId, kfId) => set((s) => ({ tracks: s.tracks.map(t => t.objectId === objectId ? { ...t, keyframes: t.keyframes.filter(k => k.id !== kfId) } : t ), })), getTrack: (objectId) => get().tracks.find(t => t.objectId === objectId), // ── Skybox ──────────────────────────────────────────────────── skyboxType: 'gradient', skyboxUrl: null, setSkybox: (type, url) => set({ skyboxType: type, skyboxUrl: url || null }), // ── Lighting ────────────────────────────────────────────────── ambientIntensity: 0.5, directionalIntensity: 1.0, bgColor: '#1a1a2e', showGrid: true, showAxes: true, postProcessing: false, bloomIntensity: 0.5, setAmbientIntensity: (v) => set({ ambientIntensity: v }), setDirectionalIntensity:(v) => set({ directionalIntensity: v }), setBgColor: (c) => set({ bgColor: c }), setShowGrid: (v) => set({ showGrid: v }), setShowAxes: (v) => set({ showAxes: v }), setPostProcessing:(v) => set({ postProcessing: v }), setBloomIntensity:(v) => set({ bloomIntensity: v }), // ── AI Error Resolver ───────────────────────────────────────── openrouterKey: '', setOpenrouterKey: (k) => set({ openrouterKey: k }), errorLogs: [], addError: (msg) => set((s) => ({ errorLogs: [...s.errorLogs, { id: Math.random().toString(36).slice(2), message: msg, timestamp: Date.now(), }], })), resolveError: (id, resolution) => set((s) => ({ errorLogs: s.errorLogs.map(e => e.id === id ? { ...e, resolved: resolution, resolving: false } : e), })), setErrorResolving: (id, v) => set((s) => ({ errorLogs: s.errorLogs.map(e => e.id === id ? { ...e, resolving: v } : e), })), clearErrors: () => set({ errorLogs: [] }), // ── Recording ───────────────────────────────────────────────── isRecording: false, setIsRecording: (v) => set({ isRecording: v }), }));