perf: compress GLBs 75%, add Draco decoder, loading spinner for attire
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { configStore, useConfigStore } from '@/store/useConfigStore';
|
||||
import { personaStore, usePersonaStore } from '@/store/usePersonaStore';
|
||||
import { PricingEngine } from './PricingEngine';
|
||||
@@ -9,10 +9,25 @@ export function ConfigPanel() {
|
||||
const activeColors = useConfigStore((s) => s.activeColors);
|
||||
const activePersona = useConfigStore((s) => s.activePersonaAttire);
|
||||
const personas = usePersonaStore((s) => s.personas);
|
||||
// Track which persona is loading (waiting for GLB to download)
|
||||
const [loadingPersona, setLoadingPersona] = useState<string | null>(null);
|
||||
|
||||
const colorsSectionRef = useRef<HTMLElement>(null);
|
||||
const personaSectionRef = useRef<HTMLElement>(null);
|
||||
|
||||
// Clear loading state once activePersona matches what we requested
|
||||
const prevActivePersona = useRef(activePersona);
|
||||
useEffect(() => {
|
||||
if (activePersona !== prevActivePersona.current) {
|
||||
prevActivePersona.current = activePersona;
|
||||
}
|
||||
// After 6s max, clear loading state even if something went wrong
|
||||
if (loadingPersona !== null) {
|
||||
const t = setTimeout(() => setLoadingPersona(null), 6000);
|
||||
return () => clearTimeout(t);
|
||||
}
|
||||
}, [activePersona, loadingPersona]);
|
||||
|
||||
useEffect(() => {
|
||||
personaStore.getState().hydrate();
|
||||
}, []);
|
||||
@@ -22,7 +37,15 @@ export function ConfigPanel() {
|
||||
}, []);
|
||||
|
||||
const handlePersonaSelect = useCallback((attire: string) => {
|
||||
// Only show loading for dynamic (uploaded) attire that has a GLB to download
|
||||
const persona = personaStore.getState().personas.find((p) => p.id === attire);
|
||||
const isStatic = ['none', 'emarati-kandura', 'industrial-vest', 'business-suit'].includes(attire);
|
||||
if (!isStatic && persona?.modelPath) {
|
||||
setLoadingPersona(attire);
|
||||
}
|
||||
configStore.getState().setPersonaAttire(attire);
|
||||
// Clear loading once the spin animation would have completed (~800ms)
|
||||
setTimeout(() => setLoadingPersona(null), 3000);
|
||||
}, []);
|
||||
|
||||
const handleReset = useCallback(() => {
|
||||
@@ -122,8 +145,16 @@ export function ConfigPanel() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Active checkmark */}
|
||||
{isActive && (
|
||||
{/* Active checkmark or loading spinner */}
|
||||
{isActive && loadingPersona === persona.id ? (
|
||||
<div style={{
|
||||
width: '20px', height: '20px', borderRadius: '50%',
|
||||
border: '2px solid rgba(59,130,246,0.2)',
|
||||
borderTopColor: '#3b82f6',
|
||||
animation: 'spin 0.8s linear infinite',
|
||||
flexShrink: 0,
|
||||
}} />
|
||||
) : isActive ? (
|
||||
<div style={{
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
@@ -138,7 +169,7 @@ export function ConfigPanel() {
|
||||
<polyline points="20 6 9 17 4 12" />
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
) : null}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -10,8 +10,11 @@ import {
|
||||
Html,
|
||||
} from '@react-three/drei';
|
||||
import { useThree } from '@react-three/fiber';
|
||||
import { useGLTF } from '@react-three/drei';
|
||||
import { RobotModel } from './RobotModel';
|
||||
import { snapshotStore } from '@/store/useSnapshotStore';
|
||||
|
||||
useGLTF.setDecoderPath('/draco/');
|
||||
import type { WebGLRenderer, Scene, Camera } from 'three';
|
||||
|
||||
function Loader() {
|
||||
@@ -132,7 +135,7 @@ export function RobotCanvas() {
|
||||
return (
|
||||
<div style={{ position: 'relative', width: '100%', height: '100%' }}>
|
||||
<Canvas
|
||||
dpr={[1, 2]}
|
||||
dpr={[1, 1.5]}
|
||||
camera={{ position: [0, 1, 5], fov: 50 }}
|
||||
gl={{ antialias: true, powerPreference: 'high-performance' }}
|
||||
style={{ background: 'linear-gradient(180deg, #e8e8e4 0%, #f0f0ec 50%, #e8e8e4 100%)' }}
|
||||
|
||||
@@ -7,6 +7,9 @@ import * as THREE from 'three';
|
||||
import { useConfigStore } from '@/store/useConfigStore';
|
||||
import { personaStore, usePersonaStore } from '@/store/usePersonaStore';
|
||||
|
||||
// Configure Draco decoder so compressed .glb files load correctly
|
||||
useGLTF.setDecoderPath('/draco/');
|
||||
|
||||
const STATIC_ATTIRE_GLB: Record<string, string> = {
|
||||
'emarati-kandura': '/Kandoura.glb',
|
||||
'industrial-vest': '/Vest.glb',
|
||||
|
||||
@@ -6,6 +6,8 @@ import { Environment, ContactShadows, Html, useProgress, useGLTF } from '@react-
|
||||
import * as THREE from 'three';
|
||||
import { RobotModel } from './RobotModel';
|
||||
|
||||
useGLTF.setDecoderPath('/draco/');
|
||||
|
||||
// Original camera keyframes - cinematic path with dramatic shots
|
||||
const CAMERA_KEYFRAMES: [number, [number, number, number]][] = [
|
||||
[0.0, [-2.5, 0.6, 7]], // Start: far left wide shot
|
||||
|
||||
Reference in New Issue
Block a user