perf: compress GLBs 75%, add Draco decoder, loading spinner for attire
CI/CD / test-and-build (push) Waiting to run
CI/CD / deploy (push) Blocked by required conditions

This commit is contained in:
Najjar\NajjarV02
2026-04-20 15:52:00 +04:00
parent b2a484f402
commit 2ff21c5b54
10 changed files with 226 additions and 5 deletions
+35 -4
View File
@@ -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>
);
})}
+4 -1
View File
@@ -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%)' }}
+3
View File
@@ -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',
+2
View File
@@ -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