first
CI/CD / test-and-build (push) Has been cancelled
CI/CD / deploy (push) Has been cancelled

This commit is contained in:
Yjoz
2026-04-10 15:31:59 +04:00
parent 87d2688eeb
commit ec0f991a30
55 changed files with 10218 additions and 0 deletions
+200
View File
@@ -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');