first
This commit is contained in:
@@ -0,0 +1,200 @@
|
||||
'use client';
|
||||
|
||||
import { useRef, useMemo, useEffect, Suspense, useState } from 'react';
|
||||
import { useGLTF } from '@react-three/drei';
|
||||
import { useFrame } from '@react-three/fiber';
|
||||
import * as THREE from 'three';
|
||||
import { useConfigStore } from '@/store/useConfigStore';
|
||||
|
||||
const ATTIRE_GLB: Record<string, string> = {
|
||||
'emarati-kandura': '/Kandoura.glb',
|
||||
'industrial-vest': '/Vest.glb',
|
||||
'business-suit': '/Suit.glb',
|
||||
};
|
||||
|
||||
// Preload all attire models so they're cached before user clicks
|
||||
Object.values(ATTIRE_GLB).forEach((path) => useGLTF.preload(path));
|
||||
|
||||
function easeInOutCubic(t: number): number {
|
||||
return t < 0.5
|
||||
? 4 * t * t * t
|
||||
: 1 - Math.pow(-2 * t + 2, 3) / 2;
|
||||
}
|
||||
|
||||
function AttireModel({ glbPath, onLoaded }: { glbPath: string; onLoaded: () => void }) {
|
||||
const { scene } = useGLTF(glbPath);
|
||||
|
||||
const processedAttire = useMemo(() => {
|
||||
const cloned = scene.clone();
|
||||
|
||||
cloned.traverse((child) => {
|
||||
if (child instanceof THREE.Mesh && child.material instanceof THREE.MeshStandardMaterial) {
|
||||
child.material.envMapIntensity = 1;
|
||||
child.material.needsUpdate = true;
|
||||
}
|
||||
});
|
||||
|
||||
const box = new THREE.Box3().setFromObject(cloned);
|
||||
const center = box.getCenter(new THREE.Vector3());
|
||||
const size = box.getSize(new THREE.Vector3());
|
||||
const maxDim = Math.max(size.x, size.y, size.z);
|
||||
const scale = 2 / maxDim;
|
||||
|
||||
cloned.scale.setScalar(scale);
|
||||
cloned.position.set(
|
||||
-center.x * scale,
|
||||
-center.y * scale + 0.5,
|
||||
-center.z * scale
|
||||
);
|
||||
|
||||
return cloned;
|
||||
}, [scene]);
|
||||
|
||||
// Signal that the model is ready and rendered
|
||||
useEffect(() => {
|
||||
onLoaded();
|
||||
}, [onLoaded]);
|
||||
|
||||
return <primitive object={processedAttire} />;
|
||||
}
|
||||
|
||||
interface RobotModelProps {
|
||||
onError?: (error: Error) => void;
|
||||
}
|
||||
|
||||
export function RobotModel({ onError }: RobotModelProps) {
|
||||
const groupRef = useRef<THREE.Group>(null);
|
||||
|
||||
const spinningRef = useRef(false);
|
||||
const spinProgressRef = useRef(0);
|
||||
const swappedRef = useRef(false);
|
||||
const targetAttireRef = useRef<string>('none');
|
||||
|
||||
const [displayedAttire, setDisplayedAttire] = useState('none');
|
||||
const [attireReady, setAttireReady] = useState(false);
|
||||
const previousAttireRef = useRef('none');
|
||||
|
||||
const { scene } = useGLTF('/Unitree_G1.glb');
|
||||
|
||||
const activeColors = useConfigStore((state) => state.activeColors);
|
||||
const activePersonaAttire = useConfigStore((state) => state.activePersonaAttire);
|
||||
|
||||
// Detect attire change and trigger spin
|
||||
useEffect(() => {
|
||||
if (activePersonaAttire !== previousAttireRef.current) {
|
||||
targetAttireRef.current = activePersonaAttire;
|
||||
spinningRef.current = true;
|
||||
spinProgressRef.current = 0;
|
||||
swappedRef.current = false;
|
||||
setAttireReady(false);
|
||||
previousAttireRef.current = activePersonaAttire;
|
||||
}
|
||||
}, [activePersonaAttire]);
|
||||
|
||||
// Spin animation loop
|
||||
useFrame((_, delta) => {
|
||||
if (!spinningRef.current || !groupRef.current) return;
|
||||
|
||||
const spinSpeed = 2.5;
|
||||
spinProgressRef.current += delta * spinSpeed;
|
||||
|
||||
const progress = Math.min(spinProgressRef.current, 1);
|
||||
const easedRotation = easeInOutCubic(progress) * Math.PI * 2;
|
||||
groupRef.current.rotation.y = easedRotation;
|
||||
|
||||
if (progress >= 0.5 && !swappedRef.current) {
|
||||
setDisplayedAttire(targetAttireRef.current);
|
||||
swappedRef.current = true;
|
||||
}
|
||||
|
||||
if (progress >= 1) {
|
||||
groupRef.current.rotation.y = 0;
|
||||
spinningRef.current = false;
|
||||
spinProgressRef.current = 0;
|
||||
}
|
||||
});
|
||||
|
||||
const processedScene = useMemo(() => {
|
||||
const clonedScene = scene.clone();
|
||||
|
||||
clonedScene.traverse((child) => {
|
||||
if (child instanceof THREE.Mesh) {
|
||||
if (!child.material) {
|
||||
child.material = new THREE.MeshStandardMaterial({
|
||||
color: '#96a2b6',
|
||||
metalness: 0.8,
|
||||
roughness: 0.2,
|
||||
});
|
||||
}
|
||||
|
||||
if (child.material instanceof THREE.MeshStandardMaterial) {
|
||||
child.material.envMapIntensity = 1;
|
||||
child.material.needsUpdate = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const box = new THREE.Box3().setFromObject(clonedScene);
|
||||
const center = box.getCenter(new THREE.Vector3());
|
||||
const size = box.getSize(new THREE.Vector3());
|
||||
|
||||
const maxDim = Math.max(size.x, size.y, size.z);
|
||||
const scale = 2 / maxDim;
|
||||
clonedScene.scale.setScalar(scale);
|
||||
|
||||
const offset = new THREE.Vector3(
|
||||
-center.x * scale,
|
||||
-center.y * scale + 0.5,
|
||||
-center.z * scale
|
||||
);
|
||||
clonedScene.position.copy(offset);
|
||||
|
||||
return clonedScene;
|
||||
}, [scene]);
|
||||
|
||||
// Hide base robot only when attire is selected AND the attire GLB has loaded
|
||||
// Show base robot when 'none' is selected or attire is still loading
|
||||
const showBase = displayedAttire === 'none' || !attireReady;
|
||||
|
||||
useEffect(() => {
|
||||
processedScene.visible = showBase;
|
||||
}, [showBase, processedScene]);
|
||||
|
||||
// Apply primary color to the base robot only
|
||||
useEffect(() => {
|
||||
if (!groupRef.current) return;
|
||||
|
||||
groupRef.current.traverse((child) => {
|
||||
if (child instanceof THREE.Mesh && child.material instanceof THREE.MeshStandardMaterial) {
|
||||
if (child.name.startsWith('Unitree_G1')) {
|
||||
child.material.color.set(activeColors.primary);
|
||||
child.material.needsUpdate = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
}, [activeColors]);
|
||||
|
||||
const attireGlbPath = ATTIRE_GLB[displayedAttire] || null;
|
||||
|
||||
const handleAttireLoaded = () => {
|
||||
setAttireReady(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<group ref={groupRef}>
|
||||
<primitive object={processedScene} />
|
||||
|
||||
{attireGlbPath && (
|
||||
<Suspense fallback={null}>
|
||||
<AttireModel
|
||||
key={attireGlbPath}
|
||||
glbPath={attireGlbPath}
|
||||
onLoaded={handleAttireLoaded}
|
||||
/>
|
||||
</Suspense>
|
||||
)}
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
useGLTF.preload('/Unitree_G1.glb');
|
||||
Reference in New Issue
Block a user