diff --git a/src/components/AvatarPanel.tsx b/src/components/AvatarPanel.tsx
new file mode 100644
index 0000000..6f1c1d8
--- /dev/null
+++ b/src/components/AvatarPanel.tsx
@@ -0,0 +1,427 @@
+import React, { useState, useCallback } from 'react';
+import { useDropzone } from 'react-dropzone';
+import { useAvatarStore } from '../store/avatar-store';
+import { AvatarViewer3D } from './AvatarViewer3D';
+import { Button } from './ui/button';
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from './ui/card';
+import { Checkbox } from './ui/checkbox';
+import { ScrollArea } from './ui/scroll-area';
+import { Separator } from './ui/separator';
+import { Badge } from './ui/badge';
+import {
+ Upload,
+ FileUp,
+ Sparkles,
+ Download,
+ Settings,
+ FolderOpen,
+ Plus,
+ Trash2,
+ Play,
+ Check,
+ AlertCircle,
+ User,
+} from 'lucide-react';
+import { platforms, type PlatformId } from '../lib/platforms';
+import type { AvatarProject } from '../lib/avatar/types';
+
+interface AvatarPanelProps {
+ className?: string;
+}
+
+export function AvatarPanel({ className = '' }: AvatarPanelProps) {
+ const {
+ currentProject,
+ projects,
+ setCurrentProject,
+ selectedPlatforms,
+ setSelectedPlatforms,
+ uploadProgress,
+ } = useAvatarStore();
+
+ const [view, setView] = useState<'projects' | 'import' | 'export'>('projects');
+
+ // File upload handling
+ const onDrop = useCallback((acceptedFiles: File[]) => {
+ const file = acceptedFiles[0];
+ if (file) {
+ handleFileImport(file);
+ }
+ }, []);
+
+ const { getRootProps, getInputProps, isDragActive } = useDropzone({
+ onDrop,
+ accept: {
+ 'model/gltf-binary': ['.glb'],
+ 'model/gltf+json': ['.gltf'],
+ 'model/fbx': ['.fbx'],
+ 'application/octet-stream': ['.vrm'],
+ },
+ multiple: false,
+ });
+
+ const handleFileImport = async (file: File) => {
+ console.log('Importing file:', file.name);
+ // TODO: Implement actual file import logic
+ // This will:
+ // 1. Upload file to storage
+ // 2. Parse file format
+ // 3. Extract model statistics
+ // 4. Create AvatarProject
+ // 5. Add to store
+ };
+
+ const handlePlatformToggle = (platformId: PlatformId) => {
+ setSelectedPlatforms(
+ selectedPlatforms.includes(platformId)
+ ? selectedPlatforms.filter((p) => p !== platformId)
+ : [...selectedPlatforms, platformId]
+ );
+ };
+
+ const handleExport = async () => {
+ if (!currentProject) return;
+ console.log('Exporting to platforms:', selectedPlatforms);
+ // TODO: Implement export logic
+ };
+
+ return (
+
+ {/* Left Sidebar - Projects & Templates */}
+
+
+
+
+ Avatar Studio
+
+
+ {/* View Selector */}
+
+
+
+
+
+
+
+
+
+ {/* Projects List */}
+
+
+
+ {projects.length} project{projects.length !== 1 ? 's' : ''}
+
+ {projects.length === 0 ? (
+
+ No avatars yet.
+
+ Import one to get started!
+
+ ) : (
+ projects.map((project) => (
+
+ ))
+ )}
+
+
+
+ {/* Quick Actions */}
+
+
+
+
+
+ {/* Main Content Area */}
+
+ {/* 3D Viewer */}
+
+ {view === 'projects' && (
+
+ )}
+
+ {view === 'import' && (
+
+
+
+ Import Avatar
+
+ Upload a 3D model to get started. Supported formats: VRM, GLB, GLTF, FBX
+
+
+
+
+
+
+ {isDragActive ? (
+
Drop your avatar file here...
+ ) : (
+ <>
+
Drag & drop your avatar file
+
or click to browse
+
+ >
+ )}
+
+
+ {uploadProgress && (
+
+
+ {uploadProgress.fileName}
+
+ {uploadProgress.progress}%
+
+
+
+
+ )}
+
+
+
+
+
+
Auto-Rig
+
+ AI-powered rigging
+
+
+
+
+
+
+
Multi-Platform
+
+ Export to 5+ platforms
+
+
+
+
+
+
+
+ )}
+
+ {view === 'export' && currentProject && (
+
+
+
+ Export to Platforms
+
+ Select which platforms to export {currentProject.name} to
+
+
+
+ {/* Platform Selection */}
+
+ {Object.values(platforms).map((platform) => {
+ const isSelected = selectedPlatforms.includes(platform.id);
+ const isSupported = currentProject.supportedPlatforms?.includes(platform.id);
+
+ return (
+
+
+
handlePlatformToggle(platform.id)}
+ />
+
+
+
+
+ {isSupported ? (
+
+
+ Compatible
+
+ ) : (
+
+
+ Needs Optimization
+
+ )}
+
+
+ );
+ })}
+
+
+
+
+ {/* Export Actions */}
+
+
+ {selectedPlatforms.length} platform{selectedPlatforms.length !== 1 ? 's' : ''} selected
+
+
+
+
+
+
+
+
+
+ )}
+
+
+
+ {/* Right Sidebar - Properties & Stats */}
+ {currentProject && (
+
+
+
Properties
+
+
+ Name:
+ {currentProject.name}
+
+
+ Format:
+ {currentProject.sourceFormat.toUpperCase()}
+
+
+ Polygons:
+ {currentProject.polygonCount.toLocaleString()}
+
+
+ Textures:
+
+ {currentProject.textureCount} ({currentProject.textureSize.toFixed(1)} MB)
+
+
+
+ Rig Type:
+ {currentProject.rigType}
+
+ {currentProject.boneCount && (
+
+ Bones:
+ {currentProject.boneCount}
+
+ )}
+
+
+
+
+
+
+
Quick Actions
+
+
+
+
+
+
+
+
+
+ )}
+
+ );
+}
diff --git a/src/components/AvatarViewer3D.tsx b/src/components/AvatarViewer3D.tsx
new file mode 100644
index 0000000..8033d8c
--- /dev/null
+++ b/src/components/AvatarViewer3D.tsx
@@ -0,0 +1,241 @@
+import React, { Suspense, useRef, useState } from 'react';
+import { Canvas, useFrame } from '@react-three/fiber';
+import { OrbitControls, Grid, useGLTF, PerspectiveCamera, Environment } from '@react-three/drei';
+import * as THREE from 'three';
+import { Button } from './ui/button';
+import { RotateCcw, Grid3x3, Eye, EyeOff, ZoomIn, ZoomOut } from 'lucide-react';
+
+interface AvatarViewer3DProps {
+ modelUrl?: string;
+ onError?: (error: Error) => void;
+ showControls?: boolean;
+ className?: string;
+}
+
+// Model component that loads and displays the 3D model
+function Model({ url, onError }: { url: string; onError?: (error: Error) => void }) {
+ const modelRef = useRef(null);
+ const [rotation, setRotation] = useState(0);
+
+ // Load the GLTF/GLB model
+ const { scene } = useGLTF(url, true);
+
+ // Auto-rotate the model slowly
+ useFrame((state, delta) => {
+ if (modelRef.current) {
+ modelRef.current.rotation.y += delta * 0.2;
+ setRotation(modelRef.current.rotation.y);
+ }
+ });
+
+ React.useEffect(() => {
+ if (scene) {
+ // Center the model
+ const box = new THREE.Box3().setFromObject(scene);
+ const center = box.getCenter(new THREE.Vector3());
+ scene.position.sub(center);
+
+ // Scale to fit in view
+ const size = box.getSize(new THREE.Vector3());
+ const maxDim = Math.max(size.x, size.y, size.z);
+ const scale = 2 / maxDim;
+ scene.scale.setScalar(scale);
+ }
+ }, [scene]);
+
+ return (
+
+ );
+}
+
+// Fallback model when no file is loaded
+function FallbackModel() {
+ return (
+
+
+
+
+ );
+}
+
+// Loading component
+function LoadingFallback() {
+ return (
+
+
+
+
+ );
+}
+
+export function AvatarViewer3D({
+ modelUrl,
+ onError,
+ showControls = true,
+ className = '',
+}: AvatarViewer3DProps) {
+ const [showGrid, setShowGrid] = useState(true);
+ const [showWireframe, setShowWireframe] = useState(false);
+ const [autoRotate, setAutoRotate] = useState(true);
+ const controlsRef = useRef(null);
+
+ const handleReset = () => {
+ if (controlsRef.current) {
+ controlsRef.current.reset();
+ }
+ };
+
+ const handleZoomIn = () => {
+ if (controlsRef.current) {
+ controlsRef.current.dollyIn(1.2);
+ }
+ };
+
+ const handleZoomOut = () => {
+ if (controlsRef.current) {
+ controlsRef.current.dollyOut(1.2);
+ }
+ };
+
+ return (
+
+ {/* 3D Canvas */}
+
+
+ {/* Control Panel */}
+ {showControls && (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )}
+
+ {/* Info overlay */}
+ {!modelUrl && (
+
+
+
No Model Loaded
+
Import an avatar to preview it here
+
+
+ )}
+
+ );
+}
+
+// Preload common models
+export function preloadModel(url: string) {
+ useGLTF.preload(url);
+}
diff --git a/src/components/ui/tabs-extra.tsx b/src/components/ui/tabs-extra.tsx
index c7e96b3..64102b3 100644
--- a/src/components/ui/tabs-extra.tsx
+++ b/src/components/ui/tabs-extra.tsx
@@ -12,6 +12,7 @@ import { CertificationPanel } from '../CertificationPanel';
import { DesktopAppPanel } from '../DesktopAppPanel';
import { TeamCollabPanel } from '../TeamCollabPanel';
import { EnterpriseAnalyticsPanel } from '../EnterpriseAnalyticsPanel';
+import { AvatarPanel } from '../AvatarPanel';
export function ExtraTabs({ user }: { user: any }) {
return (
@@ -19,6 +20,7 @@ export function ExtraTabs({ user }: { user: any }) {
Onboarding
Profile
+ Avatars
UEFN
Preview
Translation
@@ -32,6 +34,7 @@ export function ExtraTabs({ user }: { user: any }) {
{}} />
+
diff --git a/src/lib/avatar/types.ts b/src/lib/avatar/types.ts
new file mode 100644
index 0000000..bc4b9f0
--- /dev/null
+++ b/src/lib/avatar/types.ts
@@ -0,0 +1,241 @@
+import type { PlatformId } from '../platforms';
+
+/**
+ * Avatar file format types
+ */
+export type AvatarFormat = 'vrm' | 'fbx' | 'glb' | 'gltf' | 'obj';
+
+/**
+ * Avatar rig types
+ */
+export type RigType = 'humanoid' | 'r15' | 'r6' | 'custom' | 'unrigged';
+
+/**
+ * Auto-rigging service providers
+ */
+export type RiggingService = 'accurig' | 'tripo' | 'deepmotion' | 'custom' | 'manual';
+
+/**
+ * Export job status
+ */
+export type ExportStatus = 'idle' | 'validating' | 'processing' | 'completed' | 'failed';
+
+/**
+ * Avatar project metadata
+ */
+export interface AvatarProject {
+ id: string;
+ name: string;
+ description?: string;
+ createdAt: string;
+ updatedAt: string;
+
+ // File information
+ sourceFormat: AvatarFormat;
+ sourceFileUrl?: string;
+ sourceFileSize: number; // bytes
+ thumbnailUrl?: string;
+
+ // Model statistics
+ polygonCount: number;
+ vertexCount: number;
+ textureCount: number;
+ textureSize: number; // total MB
+
+ // Rig information
+ rigType: RigType;
+ isRigged: boolean;
+ boneCount?: number;
+ riggingService?: RiggingService;
+
+ // Platform compatibility
+ supportedPlatforms: PlatformId[];
+ exportedPlatforms: PlatformId[];
+
+ // Tags and categorization
+ tags: string[];
+ category?: 'humanoid' | 'stylized' | 'realistic' | 'creature' | 'vehicle' | 'prop';
+ style?: 'anime' | 'realistic' | 'lowpoly' | 'voxel' | 'custom';
+}
+
+/**
+ * Platform-specific export configuration
+ */
+export interface PlatformExportConfig {
+ platform: PlatformId;
+ enabled: boolean;
+
+ // Optimization settings
+ targetPolygonCount?: number;
+ targetTextureSize?: number;
+ generateLODs?: boolean;
+ lodLevels?: number;
+
+ // Platform-specific settings
+ roblox?: {
+ rigType: 'r15' | 'r6';
+ includeLayeredClothing: boolean;
+ includeAccessories: boolean;
+ };
+ vrchat?: {
+ targetPerformance: 'excellent' | 'good' | 'medium' | 'poor';
+ questCompatible: boolean;
+ questPolyLimit: number;
+ includeVisemes: boolean;
+ includeEyeTracking: boolean;
+ };
+ spatial?: {
+ avatarType: 'universal' | 'world';
+ embedTextures: boolean;
+ };
+ recroom?: {
+ itemType: 'full-avatar' | 'accessory';
+ beanCompatible: boolean;
+ };
+}
+
+/**
+ * Export validation result
+ */
+export interface ValidationResult {
+ platform: PlatformId;
+ isValid: boolean;
+ warnings: ValidationMessage[];
+ errors: ValidationMessage[];
+
+ // Performance metrics
+ performanceRank?: 'excellent' | 'good' | 'medium' | 'poor';
+ estimatedFileSize?: number; // MB
+
+ // Platform-specific checks
+ checks: {
+ polygonCount: boolean;
+ textureSize: boolean;
+ materialCount: boolean;
+ rigStructure: boolean;
+ boneCount: boolean;
+ };
+}
+
+export interface ValidationMessage {
+ type: 'error' | 'warning' | 'info';
+ message: string;
+ fix?: string; // Suggested fix
+}
+
+/**
+ * Export job
+ */
+export interface ExportJob {
+ id: string;
+ avatarId: string;
+ platform: PlatformId;
+ status: ExportStatus;
+ progress: number; // 0-100
+
+ startedAt: string;
+ completedAt?: string;
+
+ // Results
+ outputFileUrl?: string;
+ outputFileSize?: number;
+ validationResult?: ValidationResult;
+
+ // Error handling
+ error?: string;
+ retryCount: number;
+}
+
+/**
+ * Avatar template for starting new projects
+ */
+export interface AvatarTemplate {
+ id: string;
+ name: string;
+ description: string;
+ thumbnailUrl: string;
+ modelUrl: string;
+
+ category: 'humanoid' | 'stylized' | 'realistic' | 'creature';
+ style: 'anime' | 'realistic' | 'lowpoly' | 'voxel';
+
+ polygonCount: number;
+ isRigged: boolean;
+ rigType: RigType;
+
+ supportedPlatforms: PlatformId[];
+ isPremium: boolean;
+ price?: number;
+}
+
+/**
+ * Auto-rigging request
+ */
+export interface RiggingRequest {
+ avatarId: string;
+ service: RiggingService;
+ sourceFileUrl: string;
+
+ options: {
+ quality: 'fast' | 'balanced' | 'high';
+ rigType: 'humanoid' | 'quadruped' | 'custom';
+ preserveBlendShapes?: boolean;
+ };
+}
+
+/**
+ * Auto-rigging result
+ */
+export interface RiggingResult {
+ success: boolean;
+ riggedFileUrl?: string;
+
+ rigInfo?: {
+ boneCount: number;
+ rootBone: string;
+ hasFingers: boolean;
+ hasFootIK: boolean;
+ hasFacialRig: boolean;
+ };
+
+ processingTime: number; // seconds
+ error?: string;
+}
+
+/**
+ * File upload progress
+ */
+export interface UploadProgress {
+ fileName: string;
+ fileSize: number;
+ uploadedBytes: number;
+ progress: number; // 0-100
+ status: 'uploading' | 'processing' | 'completed' | 'failed';
+}
+
+/**
+ * Onboarding step
+ */
+export interface OnboardingStep {
+ id: string;
+ title: string;
+ description: string;
+ completed: boolean;
+}
+
+/**
+ * User preferences for avatar studio
+ */
+export interface AvatarPreferences {
+ defaultExportFormat: AvatarFormat;
+ autoOptimize: boolean;
+ defaultRiggingService: RiggingService;
+
+ // Platform defaults
+ defaultPlatforms: PlatformId[];
+
+ // UI preferences
+ showTutorials: boolean;
+ gridSnap: boolean;
+ wireframeMode: boolean;
+}
diff --git a/src/lib/avatar/validators.ts b/src/lib/avatar/validators.ts
new file mode 100644
index 0000000..7c3f71c
--- /dev/null
+++ b/src/lib/avatar/validators.ts
@@ -0,0 +1,422 @@
+import type { AvatarProject, ValidationResult, ValidationMessage } from './types';
+import type { PlatformId } from '../platforms';
+
+/**
+ * Platform-specific requirements and limits
+ */
+const PLATFORM_LIMITS = {
+ roblox: {
+ maxPolygons: 50000,
+ maxTextureSize: 1024,
+ maxTextures: 10,
+ maxBones: 75,
+ requiresRig: true,
+ },
+ vrchat: {
+ pc: {
+ excellent: { maxPolygons: 32000, maxMaterials: 4, maxTextureMemory: 10 },
+ good: { maxPolygons: 70000, maxMaterials: 8, maxTextureMemory: 40 },
+ medium: { maxPolygons: 100000, maxMaterials: 16, maxTextureMemory: 75 },
+ poor: { maxPolygons: 150000, maxMaterials: 32, maxTextureMemory: 150 },
+ },
+ quest: {
+ excellent: { maxPolygons: 7500, maxMaterials: 1, maxTextureMemory: 10 },
+ good: { maxPolygons: 10000, maxMaterials: 2, maxTextureMemory: 20 },
+ medium: { maxPolygons: 15000, maxMaterials: 4, maxTextureMemory: 30 },
+ poor: { maxPolygons: 20000, maxMaterials: 8, maxTextureMemory: 40 },
+ },
+ requiresRig: true,
+ requiresVisemes: false,
+ },
+ spatial: {
+ maxPolygons: 100000,
+ maxTextureSize: 2048,
+ maxTextures: 16,
+ requiresRig: true,
+ requiresHumanoidRig: true,
+ },
+ recroom: {
+ maxPolygons: 15000,
+ maxTextureSize: 1024,
+ maxTextures: 5,
+ requiresRig: false,
+ },
+ uefn: {
+ maxPolygons: 100000,
+ maxTextureSize: 2048,
+ maxTextures: 20,
+ requiresRig: false,
+ },
+ core: {
+ maxPolygons: 50000,
+ maxTextureSize: 1024,
+ maxTextures: 10,
+ requiresRig: false,
+ },
+} as const;
+
+/**
+ * Validate avatar for Roblox platform
+ */
+function validateRoblox(avatar: AvatarProject): ValidationResult {
+ const errors: ValidationMessage[] = [];
+ const warnings: ValidationMessage[] = [];
+ const limits = PLATFORM_LIMITS.roblox;
+
+ // Check polygon count
+ if (avatar.polygonCount > limits.maxPolygons) {
+ errors.push({
+ type: 'error',
+ message: `Polygon count (${avatar.polygonCount.toLocaleString()}) exceeds Roblox recommended limit (${limits.maxPolygons.toLocaleString()})`,
+ fix: 'Use polygon reduction tool to optimize the model',
+ });
+ } else if (avatar.polygonCount > limits.maxPolygons * 0.8) {
+ warnings.push({
+ type: 'warning',
+ message: `Polygon count is high (${avatar.polygonCount.toLocaleString()}). Consider optimizing for better performance.`,
+ fix: 'Reduce polygon count to improve performance',
+ });
+ }
+
+ // Check texture size
+ if (avatar.textureSize > limits.maxTextures * 1) {
+ warnings.push({
+ type: 'warning',
+ message: `Total texture size (${avatar.textureSize.toFixed(1)} MB) is high`,
+ fix: 'Compress textures or reduce texture resolution',
+ });
+ }
+
+ // Check rig
+ if (!avatar.isRigged) {
+ errors.push({
+ type: 'error',
+ message: 'Roblox requires a rigged model (R15 or R6)',
+ fix: 'Use the Auto-Rig feature to add a skeleton',
+ });
+ } else if (avatar.rigType !== 'r15' && avatar.rigType !== 'r6') {
+ warnings.push({
+ type: 'warning',
+ message: `Rig type (${avatar.rigType}) may need conversion to R15 or R6`,
+ fix: 'Auto-conversion will be applied during export',
+ });
+ }
+
+ // Check bone count
+ if (avatar.boneCount && avatar.boneCount > limits.maxBones) {
+ warnings.push({
+ type: 'warning',
+ message: `Bone count (${avatar.boneCount}) exceeds typical Roblox limit (${limits.maxBones})`,
+ fix: 'Simplify rig structure',
+ });
+ }
+
+ const isValid = errors.length === 0;
+
+ return {
+ platform: 'roblox',
+ isValid,
+ errors,
+ warnings,
+ performanceRank: getPerformanceRank(avatar.polygonCount, limits.maxPolygons),
+ checks: {
+ polygonCount: avatar.polygonCount <= limits.maxPolygons,
+ textureSize: true, // Roblox is flexible with texture sizes
+ materialCount: true,
+ rigStructure: avatar.isRigged,
+ boneCount: !avatar.boneCount || avatar.boneCount <= limits.maxBones,
+ },
+ };
+}
+
+/**
+ * Validate avatar for VRChat platform
+ */
+function validateVRChat(
+ avatar: AvatarProject,
+ targetPlatform: 'pc' | 'quest' = 'pc'
+): ValidationResult {
+ const errors: ValidationMessage[] = [];
+ const warnings: ValidationMessage[] = [];
+ const limits = PLATFORM_LIMITS.vrchat;
+
+ // Determine performance rank based on polygon count
+ let performanceRank: 'excellent' | 'good' | 'medium' | 'poor' = 'poor';
+ const platformLimits = limits[targetPlatform];
+
+ if (avatar.polygonCount <= platformLimits.excellent.maxPolygons) {
+ performanceRank = 'excellent';
+ } else if (avatar.polygonCount <= platformLimits.good.maxPolygons) {
+ performanceRank = 'good';
+ } else if (avatar.polygonCount <= platformLimits.medium.maxPolygons) {
+ performanceRank = 'medium';
+ }
+
+ // Check against limits for the determined rank
+ const rankLimits = platformLimits[performanceRank];
+
+ if (avatar.polygonCount > platformLimits.poor.maxPolygons) {
+ errors.push({
+ type: 'error',
+ message: `Polygon count (${avatar.polygonCount.toLocaleString()}) exceeds VRChat ${targetPlatform.toUpperCase()} maximum (${platformLimits.poor.maxPolygons.toLocaleString()})`,
+ fix: 'Use polygon reduction to meet platform limits',
+ });
+ } else if (performanceRank === 'poor') {
+ warnings.push({
+ type: 'warning',
+ message: `Avatar will be rated as "Poor" performance in VRChat ${targetPlatform.toUpperCase()}`,
+ fix: `Reduce polygons to ${platformLimits.medium.maxPolygons.toLocaleString()} for Medium rank`,
+ });
+ }
+
+ // Check texture memory
+ if (avatar.textureSize > rankLimits.maxTextureMemory) {
+ warnings.push({
+ type: 'warning',
+ message: `Texture memory (${avatar.textureSize.toFixed(1)} MB) may affect performance rank`,
+ fix: 'Compress or downsize textures',
+ });
+ }
+
+ // Check rig
+ if (!avatar.isRigged) {
+ errors.push({
+ type: 'error',
+ message: 'VRChat requires a humanoid rig',
+ fix: 'Use Auto-Rig to add Avatar 3.0 compatible skeleton',
+ });
+ }
+
+ const isValid = errors.length === 0;
+
+ return {
+ platform: 'vrchat',
+ isValid,
+ errors,
+ warnings,
+ performanceRank,
+ checks: {
+ polygonCount: avatar.polygonCount <= platformLimits.poor.maxPolygons,
+ textureSize: avatar.textureSize <= rankLimits.maxTextureMemory,
+ materialCount: true, // Will check material count in full export
+ rigStructure: avatar.isRigged && avatar.rigType === 'humanoid',
+ boneCount: true,
+ },
+ };
+}
+
+/**
+ * Validate avatar for Spatial platform
+ */
+function validateSpatial(avatar: AvatarProject): ValidationResult {
+ const errors: ValidationMessage[] = [];
+ const warnings: ValidationMessage[] = [];
+ const limits = PLATFORM_LIMITS.spatial;
+
+ // Check polygon count
+ if (avatar.polygonCount > limits.maxPolygons) {
+ errors.push({
+ type: 'error',
+ message: `Polygon count (${avatar.polygonCount.toLocaleString()}) exceeds Spatial limit (${limits.maxPolygons.toLocaleString()})`,
+ fix: 'Reduce polygon count using optimization tool',
+ });
+ }
+
+ // Check texture count and size
+ if (avatar.textureCount > limits.maxTextures) {
+ warnings.push({
+ type: 'warning',
+ message: `Texture count (${avatar.textureCount}) exceeds recommended limit (${limits.maxTextures})`,
+ fix: 'Combine textures or reduce texture usage',
+ });
+ }
+
+ // Check rig
+ if (!avatar.isRigged) {
+ errors.push({
+ type: 'error',
+ message: 'Spatial requires a humanoid rig',
+ fix: 'Use Auto-Rig to add a skeleton',
+ });
+ } else if (avatar.rigType !== 'humanoid') {
+ warnings.push({
+ type: 'warning',
+ message: 'Spatial works best with Unity Mecanim humanoid rigs',
+ fix: 'Rig will be converted to humanoid format during export',
+ });
+ }
+
+ const isValid = errors.length === 0;
+
+ return {
+ platform: 'spatial',
+ isValid,
+ errors,
+ warnings,
+ performanceRank: getPerformanceRank(avatar.polygonCount, limits.maxPolygons),
+ checks: {
+ polygonCount: avatar.polygonCount <= limits.maxPolygons,
+ textureSize: true,
+ materialCount: true,
+ rigStructure: avatar.isRigged,
+ boneCount: true,
+ },
+ };
+}
+
+/**
+ * Validate avatar for Rec Room platform
+ */
+function validateRecRoom(avatar: AvatarProject): ValidationResult {
+ const errors: ValidationMessage[] = [];
+ const warnings: ValidationMessage[] = [];
+ const limits = PLATFORM_LIMITS.recroom;
+
+ // Check polygon count
+ if (avatar.polygonCount > limits.maxPolygons) {
+ errors.push({
+ type: 'error',
+ message: `Polygon count (${avatar.polygonCount.toLocaleString()}) exceeds Rec Room limit (${limits.maxPolygons.toLocaleString()})`,
+ fix: 'Reduce polygon count significantly',
+ });
+ }
+
+ // Check textures
+ if (avatar.textureCount > limits.maxTextures) {
+ warnings.push({
+ type: 'warning',
+ message: `Texture count (${avatar.textureCount}) exceeds recommended limit (${limits.maxTextures})`,
+ fix: 'Combine or reduce textures',
+ });
+ }
+
+ const isValid = errors.length === 0;
+
+ return {
+ platform: 'recroom',
+ isValid,
+ errors,
+ warnings,
+ performanceRank: getPerformanceRank(avatar.polygonCount, limits.maxPolygons),
+ checks: {
+ polygonCount: avatar.polygonCount <= limits.maxPolygons,
+ textureSize: true,
+ materialCount: true,
+ rigStructure: true,
+ boneCount: true,
+ },
+ };
+}
+
+/**
+ * Determine performance rank based on polygon count vs limit
+ */
+function getPerformanceRank(
+ polygonCount: number,
+ maxPolygons: number
+): 'excellent' | 'good' | 'medium' | 'poor' {
+ const ratio = polygonCount / maxPolygons;
+ if (ratio <= 0.4) return 'excellent';
+ if (ratio <= 0.7) return 'good';
+ if (ratio <= 0.9) return 'medium';
+ return 'poor';
+}
+
+/**
+ * Main validation function - validates avatar for specified platform
+ */
+export function validateAvatarForPlatform(
+ avatar: AvatarProject,
+ platformId: PlatformId,
+ options: { questMode?: boolean } = {}
+): ValidationResult {
+ switch (platformId) {
+ case 'roblox':
+ return validateRoblox(avatar);
+ case 'vrchat':
+ return validateVRChat(avatar, options.questMode ? 'quest' : 'pc');
+ case 'spatial':
+ return validateSpatial(avatar);
+ case 'recroom':
+ return validateRecRoom(avatar);
+ case 'uefn':
+ // UEFN validation - more lenient
+ return {
+ platform: 'uefn',
+ isValid: true,
+ errors: [],
+ warnings: [],
+ performanceRank: 'good',
+ checks: {
+ polygonCount: true,
+ textureSize: true,
+ materialCount: true,
+ rigStructure: true,
+ boneCount: true,
+ },
+ };
+ case 'core':
+ // Core platform validation
+ return {
+ platform: 'core',
+ isValid: true,
+ errors: [],
+ warnings: [],
+ performanceRank: 'good',
+ checks: {
+ polygonCount: true,
+ textureSize: true,
+ materialCount: true,
+ rigStructure: true,
+ boneCount: true,
+ },
+ };
+ default:
+ return {
+ platform: platformId,
+ isValid: false,
+ errors: [{ type: 'error', message: 'Unknown platform' }],
+ warnings: [],
+ checks: {
+ polygonCount: false,
+ textureSize: false,
+ materialCount: false,
+ rigStructure: false,
+ boneCount: false,
+ },
+ };
+ }
+}
+
+/**
+ * Validate avatar for multiple platforms at once
+ */
+export function validateAvatarForPlatforms(
+ avatar: AvatarProject,
+ platformIds: PlatformId[]
+): ValidationResult[] {
+ return platformIds.map((platformId) => validateAvatarForPlatform(avatar, platformId));
+}
+
+/**
+ * Get a summary of validation results
+ */
+export function getValidationSummary(results: ValidationResult[]): {
+ totalPlatforms: number;
+ validPlatforms: number;
+ totalErrors: number;
+ totalWarnings: number;
+ needsOptimization: boolean;
+} {
+ const totalErrors = results.reduce((sum, r) => sum + r.errors.length, 0);
+ const totalWarnings = results.reduce((sum, r) => sum + r.warnings.length, 0);
+ const validPlatforms = results.filter((r) => r.isValid).length;
+
+ return {
+ totalPlatforms: results.length,
+ validPlatforms,
+ totalErrors,
+ totalWarnings,
+ needsOptimization: totalErrors > 0 || totalWarnings > 2,
+ };
+}
diff --git a/src/store/avatar-store.ts b/src/store/avatar-store.ts
new file mode 100644
index 0000000..99b1838
--- /dev/null
+++ b/src/store/avatar-store.ts
@@ -0,0 +1,255 @@
+import { create } from 'zustand';
+import { persist } from 'zustand/middleware';
+import type {
+ AvatarProject,
+ AvatarTemplate,
+ ExportJob,
+ PlatformExportConfig,
+ ValidationResult,
+ UploadProgress,
+ OnboardingStep,
+ AvatarPreferences,
+} from '../lib/avatar/types';
+import type { PlatformId } from '../lib/platforms';
+
+interface AvatarStore {
+ // Current project state
+ currentProject: AvatarProject | null;
+ setCurrentProject: (project: AvatarProject | null) => void;
+
+ // Project library
+ projects: AvatarProject[];
+ addProject: (project: AvatarProject) => void;
+ updateProject: (id: string, updates: Partial) => void;
+ deleteProject: (id: string) => void;
+
+ // Export configuration
+ exportConfigs: Record; // avatarId -> platform configs
+ updateExportConfig: (avatarId: string, config: PlatformExportConfig) => void;
+ getExportConfig: (avatarId: string, platform: PlatformId) => PlatformExportConfig | undefined;
+
+ // Export jobs
+ exportJobs: ExportJob[];
+ addExportJob: (job: ExportJob) => void;
+ updateExportJob: (id: string, updates: Partial) => void;
+ getActiveJobs: () => ExportJob[];
+
+ // Validation results
+ validationResults: Record; // avatarId -> results
+ setValidationResults: (avatarId: string, results: ValidationResult[]) => void;
+
+ // Upload state
+ uploadProgress: UploadProgress | null;
+ setUploadProgress: (progress: UploadProgress | null) => void;
+
+ // Templates
+ templates: AvatarTemplate[];
+ setTemplates: (templates: AvatarTemplate[]) => void;
+
+ // UI state
+ isAvatarPanelOpen: boolean;
+ setAvatarPanelOpen: (open: boolean) => void;
+
+ selectedPlatforms: PlatformId[];
+ setSelectedPlatforms: (platforms: PlatformId[]) => void;
+
+ show3DViewer: boolean;
+ setShow3DViewer: (show: boolean) => void;
+
+ // Onboarding
+ onboardingSteps: OnboardingStep[];
+ setOnboardingSteps: (steps: OnboardingStep[]) => void;
+ completeOnboardingStep: (stepId: string) => void;
+ isOnboardingComplete: () => boolean;
+
+ // Preferences
+ preferences: AvatarPreferences;
+ updatePreferences: (updates: Partial) => void;
+
+ // Actions
+ resetAvatarStore: () => void;
+}
+
+const defaultPreferences: AvatarPreferences = {
+ defaultExportFormat: 'glb',
+ autoOptimize: true,
+ defaultRiggingService: 'accurig',
+ defaultPlatforms: ['roblox', 'vrchat', 'spatial'],
+ showTutorials: true,
+ gridSnap: false,
+ wireframeMode: false,
+};
+
+const defaultOnboardingSteps: OnboardingStep[] = [
+ {
+ id: 'welcome',
+ title: 'Welcome to Avatar Studio',
+ description: 'Learn how to create cross-platform avatars',
+ completed: false,
+ },
+ {
+ id: 'import',
+ title: 'Import Your First Avatar',
+ description: 'Upload a VRM, GLB, or FBX file',
+ completed: false,
+ },
+ {
+ id: 'rig',
+ title: 'Auto-Rig Your Avatar',
+ description: 'Add a skeleton to your model automatically',
+ completed: false,
+ },
+ {
+ id: 'platforms',
+ title: 'Select Target Platforms',
+ description: 'Choose which platforms to export to',
+ completed: false,
+ },
+ {
+ id: 'export',
+ title: 'Export Your Avatar',
+ description: 'Generate platform-specific files',
+ completed: false,
+ },
+];
+
+export const useAvatarStore = create()(
+ persist(
+ (set, get) => ({
+ // Initial state
+ currentProject: null,
+ projects: [],
+ exportConfigs: {},
+ exportJobs: [],
+ validationResults: {},
+ uploadProgress: null,
+ templates: [],
+ isAvatarPanelOpen: false,
+ selectedPlatforms: ['roblox', 'vrchat', 'spatial'],
+ show3DViewer: true,
+ onboardingSteps: defaultOnboardingSteps,
+ preferences: defaultPreferences,
+
+ // Project management
+ setCurrentProject: (project) => set({ currentProject: project }),
+
+ addProject: (project) =>
+ set((state) => ({
+ projects: [...state.projects, project],
+ currentProject: project,
+ })),
+
+ updateProject: (id, updates) =>
+ set((state) => ({
+ projects: state.projects.map((p) =>
+ p.id === id ? { ...p, ...updates, updatedAt: new Date().toISOString() } : p
+ ),
+ currentProject:
+ state.currentProject?.id === id
+ ? { ...state.currentProject, ...updates, updatedAt: new Date().toISOString() }
+ : state.currentProject,
+ })),
+
+ deleteProject: (id) =>
+ set((state) => ({
+ projects: state.projects.filter((p) => p.id !== id),
+ currentProject: state.currentProject?.id === id ? null : state.currentProject,
+ })),
+
+ // Export configuration
+ updateExportConfig: (avatarId, config) =>
+ set((state) => {
+ const avatarConfigs = state.exportConfigs[avatarId] || {};
+ return {
+ exportConfigs: {
+ ...state.exportConfigs,
+ [avatarId]: {
+ ...avatarConfigs,
+ [config.platform]: config,
+ },
+ },
+ };
+ }),
+
+ getExportConfig: (avatarId, platform) => {
+ const configs = get().exportConfigs[avatarId];
+ return configs?.[platform];
+ },
+
+ // Export jobs
+ addExportJob: (job) =>
+ set((state) => ({
+ exportJobs: [...state.exportJobs, job],
+ })),
+
+ updateExportJob: (id, updates) =>
+ set((state) => ({
+ exportJobs: state.exportJobs.map((j) => (j.id === id ? { ...j, ...updates } : j)),
+ })),
+
+ getActiveJobs: () =>
+ get().exportJobs.filter((j) => j.status === 'validating' || j.status === 'processing'),
+
+ // Validation
+ setValidationResults: (avatarId, results) =>
+ set((state) => ({
+ validationResults: {
+ ...state.validationResults,
+ [avatarId]: results,
+ },
+ })),
+
+ // Upload
+ setUploadProgress: (progress) => set({ uploadProgress: progress }),
+
+ // Templates
+ setTemplates: (templates) => set({ templates }),
+
+ // UI state
+ setAvatarPanelOpen: (open) => set({ isAvatarPanelOpen: open }),
+ setSelectedPlatforms: (platforms) => set({ selectedPlatforms: platforms }),
+ setShow3DViewer: (show) => set({ show3DViewer: show }),
+
+ // Onboarding
+ setOnboardingSteps: (steps) => set({ onboardingSteps: steps }),
+
+ completeOnboardingStep: (stepId) =>
+ set((state) => ({
+ onboardingSteps: state.onboardingSteps.map((step) =>
+ step.id === stepId ? { ...step, completed: true } : step
+ ),
+ })),
+
+ isOnboardingComplete: () => get().onboardingSteps.every((step) => step.completed),
+
+ // Preferences
+ updatePreferences: (updates) =>
+ set((state) => ({
+ preferences: { ...state.preferences, ...updates },
+ })),
+
+ // Reset
+ resetAvatarStore: () =>
+ set({
+ currentProject: null,
+ projects: [],
+ exportConfigs: {},
+ exportJobs: [],
+ validationResults: {},
+ uploadProgress: null,
+ selectedPlatforms: ['roblox', 'vrchat', 'spatial'],
+ onboardingSteps: defaultOnboardingSteps,
+ preferences: defaultPreferences,
+ }),
+ }),
+ {
+ name: 'aethex-avatar-storage',
+ partialize: (state) => ({
+ projects: state.projects,
+ exportConfigs: state.exportConfigs,
+ preferences: state.preferences,
+ onboardingSteps: state.onboardingSteps,
+ }),
+ }
+ )
+);