🚀 Auto-deploy: melhoria no snap e medição AR em 25/05/2026 11:46:40

This commit is contained in:
2026-05-25 11:46:40 +00:00
parent b949a7c227
commit 35881620b1
+173 -51
View File
@@ -89,16 +89,18 @@ const CHAIRS = [
];
const AVATAR_PRESETS: AvatarPreset[] = [
{ id: 1, name: 'Cyber', desc: 'Cabelo curto preto e óculos vermelhos', color: '#ef4444' },
{ id: 2, name: 'Classic', desc: 'Pele média com Cabelo castanho clássico', color: '#b45309' },
{ id: 3, name: 'Goldie', desc: 'Cabelo loiro longo com olhos azuis expressivos', color: '#eab308' },
{ id: 4, name: 'Sporty', desc: 'Boné azul virado para trás e sorriso aberto', color: '#3b82f6' },
{ id: 5, name: 'Ginger', desc: 'Cabelo ruivo longo ondulado e expressivo', color: '#f97316' },
{ id: 6, name: 'Sleek', desc: 'Pele parda com cabelo curto liso lateral', color: '#10b981' },
{ id: 7, name: 'Beard', desc: 'Careca estilosa com barba e bigode', color: '#78350f' },
{ id: 8, name: 'Sun', desc: 'Cabelo raspado e óculos escuros pretos', color: '#1e293b' },
{ id: 9, name: 'Synth', desc: 'Cabelo roxo ondulado moderno', color: '#a855f7' },
{ id: 10, name: 'Straw', desc: 'Chapéu de palha do campo clássico', color: '#ca8a04' },
// Homens (5)
{ id: 1, name: 'Carlos', desc: 'Estilo corporativo masculino clássico', url: 'https://models.readyplayer.me/65851410d54020a5605d8f63.glb?bodyType=halfbody', color: '#ef4444' },
{ id: 2, name: 'Lucas', desc: 'Estilo moderno masculino com barba', url: 'https://models.readyplayer.me/6585144b248a3064be486a4f.glb?bodyType=halfbody', color: '#b45309' },
{ id: 3, name: 'Gabriel', desc: 'Jovem e sorridente com cabelo escuro', url: 'https://models.readyplayer.me/65851480f2d52f672d6199bc.glb?bodyType=halfbody', color: '#eab308' },
{ id: 4, name: 'Marcos', desc: 'Visual executivo com óculos de grau', url: 'https://models.readyplayer.me/658514b8d54020a5605d92df.glb?bodyType=halfbody', color: '#3b82f6' },
{ id: 5, name: 'Ricardo', desc: 'Design casual moderno e cabelos grisalhos', url: 'https://models.readyplayer.me/658514ec248a3064be486f0b.glb?bodyType=halfbody', color: '#f97316' },
// Mulheres (5)
{ id: 6, name: 'Juliana', desc: 'Visual feminino clássico executivo', url: 'https://models.readyplayer.me/65851336d54020a5605d820f.glb?bodyType=halfbody', color: '#10b981' },
{ id: 7, name: 'Fernanda', desc: 'Cabelo longo escuro e roupa corporativa', url: 'https://models.readyplayer.me/65851375248a3064be485d1d.glb?bodyType=halfbody', color: '#78350f' },
{ id: 8, name: 'Camila', desc: 'Estilo moderno e cabelos cacheados', url: 'https://models.readyplayer.me/658513a9f2d52f672d61937f.glb?bodyType=halfbody', color: '#1e293b' },
{ id: 9, name: 'Beatriz', desc: 'Cabelo ruivo e trajes executivos finos', url: 'https://models.readyplayer.me/658513dbf2d52f672d6195be.glb?bodyType=halfbody', color: '#a855f7' },
{ id: 10, name: 'Amanda', desc: 'Visual jovem loira e roupas elegantes', url: 'https://models.readyplayer.me/65851403248a3064be48651a.glb?bodyType=halfbody', color: '#ca8a04' },
];
const SKIN_COLORS = ['#fed7aa', '#fdba74', '#f59e0b', '#d97706', '#9a3412', '#ffedd5'];
@@ -122,8 +124,10 @@ const getAvatarBgClass = (avatarId: number): string => {
// --- Subcomponentes 3D ---
// Avatar estilo Mii (Nintendo Wii) - Versão Melhorada (Mais Feliz e Expressivo)
function MiiAvatar({
// ==========================================
// BACKUP DOS AVATARES PROCEDURAIS ORIGINAIS
// ==========================================
function MiiAvatarBackup({
avatarId,
username,
isTalking,
@@ -142,10 +146,8 @@ function MiiAvatar({
const lastSenderRef = useRef(isLastChatSender);
const pulseEndTimeRef = useRef(0);
// Monitora quando este avatar se torna o último a escrever no chat
useEffect(() => {
if (isLastChatSender && !lastSenderRef.current) {
// Pulso rápido de 1.5 segundos (pisca 3 vezes rápido)
pulseEndTimeRef.current = Date.now() + 1500;
}
lastSenderRef.current = isLastChatSender;
@@ -168,33 +170,26 @@ function MiiAvatar({
}, [avatarId]);
useFrame((state) => {
// Animação de flutuação original
if (groupRef.current && props.position && Array.isArray(props.position) && props.position.length >= 2) {
groupRef.current.position.y = (props.position[1] as number) + Math.sin(state.clock.getElapsedTime() * 2 + avatarId) * 0.012;
}
// Animação avançada do anel de fala / chat
if (ringRef.current) {
const now = Date.now();
const material = ringRef.current.material as THREE.MeshBasicMaterial;
if (isTalking) {
// Modo Microfone: Azul
material.color.set('#3b82f6');
material.opacity = 0.6 + Math.sin(state.clock.getElapsedTime() * 10) * 0.3;
ringRef.current.scale.set(1, 1, 1);
} else if (isLastChatSender) {
// Modo Chat: Amarelo
material.color.set('#eab308');
const isPulsing = now < pulseEndTimeRef.current;
if (isPulsing) {
// Pulsação rápida inicial
material.opacity = 0.5 + Math.sin(state.clock.getElapsedTime() * 25) * 0.4;
const s = 1.0 + Math.sin(state.clock.getElapsedTime() * 25) * 0.15;
ringRef.current.scale.set(s, s, 1);
} else {
// Amarelo constante pós-pulsação
material.opacity = 0.75;
ringRef.current.scale.set(1, 1, 1);
}
@@ -218,7 +213,6 @@ function MiiAvatar({
return (
<group ref={groupRef} position={positionVector} rotation={rotationEuler} scale={scale}>
{/* Balão de Nome acima da cabeça */}
<group position={[0, 0.28, 0]}>
<mesh position={[0, 0, -0.001]}>
<planeGeometry args={[0.22, 0.05]} />
@@ -229,33 +223,24 @@ function MiiAvatar({
</Text>
</group>
{/* Indicador de Fala & Chat (anel abaixo da cabeça e 50% menor) */}
{(isTalking || isLastChatSender) && (
<mesh
ref={ringRef}
rotation={[-Math.PI / 2, 0, 0]}
position={[0, -0.01, 0]}
>
<mesh ref={ringRef} rotation={[-Math.PI / 2, 0, 0]} position={[0, -0.01, 0]}>
<ringGeometry args={[0.065, 0.075, 32]} />
<meshBasicMaterial color="#3b82f6" transparent opacity={0.7} side={THREE.DoubleSide} />
</mesh>
)}
{/* CABEÇA AVATAR (Esfera ovalada de pele) */}
<mesh position={[0, 0.1, 0]} scale={[1, 1.15, 1]}>
<sphereGeometry args={[0.08, 32, 32]} />
<meshStandardMaterial color={mii.skin} roughness={0.6} />
</mesh>
{/* CABELO MELHORADO */}
{mii.hairStyle === 'short' && (
<group>
{/* Topo do cabelo */}
<mesh position={[0, 0.15, -0.015]} scale={[1.05, 0.8, 1.05]}>
<sphereGeometry args={[0.08, 16, 16]} />
<meshStandardMaterial color={mii.hairColor} roughness={0.8} />
</mesh>
{/* Costeletas laterais */}
<mesh position={[-0.078, 0.11, 0.02]} scale={[0.2, 0.5, 0.4]}>
<sphereGeometry args={[0.03, 8, 8]} />
<meshStandardMaterial color={mii.hairColor} roughness={0.8} />
@@ -269,17 +254,14 @@ function MiiAvatar({
{mii.hairStyle === 'long' && (
<group>
{/* Cabelo Superior */}
<mesh position={[0, 0.16, -0.01]} scale={[1.05, 0.7, 1.05]}>
<sphereGeometry args={[0.08, 16, 16]} />
<meshStandardMaterial color={mii.hairColor} roughness={0.8} />
</mesh>
{/* Franja frontal */}
<mesh position={[0, 0.16, 0.045]} scale={[1.3, 0.3, 0.4]} rotation={[0.1, 0, 0]}>
<sphereGeometry args={[0.04, 16, 16]} />
<meshStandardMaterial color={mii.hairColor} roughness={0.8} />
</mesh>
{/* Costeletas longas */}
<mesh position={[-0.076, 0.05, 0.01]} scale={[0.3, 1.6, 0.6]}>
<sphereGeometry args={[0.038, 16, 16]} />
<meshStandardMaterial color={mii.hairColor} roughness={0.8} />
@@ -293,17 +275,14 @@ function MiiAvatar({
{mii.hairStyle === 'modern' && (
<group>
{/* Cabelo Superior */}
<mesh position={[0, 0.16, -0.01]} scale={[1.05, 0.7, 1.05]}>
<sphereGeometry args={[0.08, 16, 16]} />
<meshStandardMaterial color={mii.hairColor} roughness={0.8} />
</mesh>
{/* Topete Moderno */}
<mesh position={[0, 0.19, 0.02]} rotation={[-0.3, 0, 0]} scale={[0.8, 1.2, 0.8]}>
<coneGeometry args={[0.04, 0.08, 4]} />
<meshStandardMaterial color={mii.hairColor} roughness={0.8} />
</mesh>
{/* Costeletas */}
<mesh position={[-0.076, 0.09, 0.01]} scale={[0.3, 1.0, 0.5]}>
<sphereGeometry args={[0.03, 8, 8]} />
<meshStandardMaterial color={mii.hairColor} roughness={0.8} />
@@ -315,15 +294,12 @@ function MiiAvatar({
</group>
)}
{/* BONÉ */}
{mii.hasCap && (
<group position={[0, 0.17, 0.01]}>
{/* Copa do Boné */}
<mesh scale={[1.05, 0.65, 1.05]}>
<sphereGeometry args={[0.08, 16, 16]} />
<meshStandardMaterial color={mii.capColor} roughness={0.5} />
</mesh>
{/* Aba do Boné */}
<mesh position={[0, 0, -0.08]} rotation={[0.1, 0, 0]}>
<boxGeometry args={[0.11, 0.008, 0.08]} />
<meshStandardMaterial color={mii.capColor} roughness={0.5} />
@@ -331,15 +307,12 @@ function MiiAvatar({
</group>
)}
{/* CHAPÉU DE PALHA */}
{mii.hasHat && (
<group position={[0, 0.17, 0]}>
{/* Copa do Chapéu */}
<mesh>
<cylinderGeometry args={[0.055, 0.065, 0.05, 16]} />
<meshStandardMaterial color={mii.hatColor} roughness={0.8} />
</mesh>
{/* Aba do Chapéu */}
<mesh position={[0, -0.02, 0]}>
<cylinderGeometry args={[0.12, 0.12, 0.006, 16]} />
<meshStandardMaterial color={mii.hatColor} roughness={0.8} />
@@ -347,7 +320,6 @@ function MiiAvatar({
</group>
)}
{/* OLHOS MELHORADOS (Mais expressivos e com brilho) */}
<group position={[-0.028, 0.11, 0.07]} rotation={[0.08, -0.15, 0]}>
<mesh>
<sphereGeometry args={[0.011, 16, 16]} />
@@ -370,7 +342,6 @@ function MiiAvatar({
</mesh>
</group>
{/* SOBRANCELHAS */}
<mesh position={[-0.028, 0.128, 0.072]} rotation={[0.05, -0.15, 0.08]}>
<boxGeometry args={[0.028, 0.005, 0.005]} />
<meshBasicMaterial color="#18181b" />
@@ -380,13 +351,11 @@ function MiiAvatar({
<meshBasicMaterial color="#18181b" />
</mesh>
{/* NARIZ */}
<mesh position={[0, 0.09, 0.076]} scale={[1, 1.2, 1.2]}>
<sphereGeometry args={[0.008, 8, 8]} />
<meshStandardMaterial color={mii.skin} roughness={0.6} />
</mesh>
{/* BOCHECHAS ROSADAS (Deixa o avatar super amigável e feliz) */}
<mesh position={[-0.042, 0.08, 0.071]} scale={[1, 0.6, 0.2]}>
<sphereGeometry args={[0.01, 16, 16]} />
<meshBasicMaterial color="#f43f5e" transparent opacity={0.5} />
@@ -396,13 +365,11 @@ function MiiAvatar({
<meshBasicMaterial color="#f43f5e" transparent opacity={0.5} />
</mesh>
{/* BOCA FELIZ E SORRIDENTE (Sem aparência de triste) */}
<mesh position={[0, 0.062, 0.073]} rotation={[0, 0, Math.PI]}>
<torusGeometry args={[0.014, 0.003, 8, 24, Math.PI]} />
<meshBasicMaterial color="#b91c1c" />
</mesh>
{/* BARBA E BIGODE */}
{mii.beard && (
<group>
<mesh position={[0, 0.075, 0.075]}>
@@ -416,7 +383,6 @@ function MiiAvatar({
</group>
)}
{/* ÓCULOS */}
{mii.hasGlasses && (
<group position={[0, 0.11, 0.072]}>
<mesh position={[-0.028, 0, 0.002]}>
@@ -437,6 +403,162 @@ function MiiAvatar({
);
}
// ==========================================
// NOVOS AVATARES REALISTAS (READY PLAYER ME)
// ==========================================
function MiiAvatar({
avatarId,
username,
isTalking,
scale = 1,
isLastChatSender = false,
...props
}: {
avatarId: number;
username: string;
isTalking?: boolean;
scale?: number;
isLastChatSender?: boolean;
} & Record<string, unknown>) {
const groupRef = useRef<THREE.Group>(null);
const ringRef = useRef<THREE.Mesh>(null);
const lastSenderRef = useRef(isLastChatSender);
const pulseEndTimeRef = useRef(0);
// Monitora quando este avatar se torna o último a escrever no chat
useEffect(() => {
if (isLastChatSender && !lastSenderRef.current) {
pulseEndTimeRef.current = Date.now() + 1500;
}
lastSenderRef.current = isLastChatSender;
}, [isLastChatSender]);
// Obtém o preset correto para pegar a URL do avatar GLB
const preset = useMemo(() => {
return AVATAR_PRESETS.find((p) => p.id === avatarId) || AVATAR_PRESETS[0];
}, [avatarId]);
// Carrega o modelo 3D GLB de forma assíncrona
const { scene } = useGLTF(preset.url);
const clone = useMemo(() => scene.clone(), [scene]);
// Configuração inicial do clone para sombra e materiais
useEffect(() => {
if (clone) {
clone.traverse((child) => {
if (child instanceof THREE.Mesh) {
child.castShadow = true;
child.receiveShadow = true;
if (child.material) {
child.material.roughness = 0.8;
}
}
});
}
}, [clone]);
useFrame((state) => {
// Flutuação sutil de respiração/repouso
if (groupRef.current && props.position && Array.isArray(props.position) && props.position.length >= 2) {
groupRef.current.position.y = (props.position[1] as number) + Math.sin(state.clock.getElapsedTime() * 1.5 + avatarId) * 0.008 - 0.22;
}
// Animação de sincronização labial (lip-sync) nativa baseada em Morph Targets
if (clone) {
clone.traverse((child) => {
if (child instanceof THREE.SkinnedMesh && child.morphTargetInfluences) {
const dict = child.morphTargetDictionary;
if (dict) {
const jawOpenIdx = dict['jawOpen'] ?? dict['viseme_sil'] ?? dict['mouthOpen'];
const mouthSmileIdx = dict['mouthSmile'] ?? dict['mouthSmileLeft'];
if (jawOpenIdx !== undefined) {
if (isTalking) {
// Abre e fecha a boca no ritmo do clock
child.morphTargetInfluences[jawOpenIdx] = 0.15 + Math.abs(Math.sin(state.clock.getElapsedTime() * 18)) * 0.6;
} else {
child.morphTargetInfluences[jawOpenIdx] = 0;
}
}
if (mouthSmileIdx !== undefined) {
child.morphTargetInfluences[mouthSmileIdx] = 0.25;
}
}
}
});
}
// Animação do anel de fala / chat
if (ringRef.current) {
const now = Date.now();
const material = ringRef.current.material as THREE.MeshBasicMaterial;
if (isTalking) {
material.color.set('#3b82f6');
material.opacity = 0.6 + Math.sin(state.clock.getElapsedTime() * 10) * 0.3;
ringRef.current.scale.set(1, 1, 1);
} else if (isLastChatSender) {
material.color.set('#eab308');
const isPulsing = now < pulseEndTimeRef.current;
if (isPulsing) {
material.opacity = 0.5 + Math.sin(state.clock.getElapsedTime() * 25) * 0.4;
const s = 1.0 + Math.sin(state.clock.getElapsedTime() * 25) * 0.15;
ringRef.current.scale.set(s, s, 1);
} else {
material.opacity = 0.75;
ringRef.current.scale.set(1, 1, 1);
}
}
}
});
const positionVector = useMemo(() => {
if (props.position && Array.isArray(props.position)) {
return new THREE.Vector3(props.position[0] as number, (props.position[1] as number) - 0.22, props.position[2] as number);
}
return new THREE.Vector3(0, -0.22, 0);
}, [props.position]);
const rotationEuler = useMemo(() => {
if (props.rotation && Array.isArray(props.rotation)) {
return new THREE.Euler(props.rotation[0] as number, props.rotation[1] as number, props.rotation[2] as number);
}
return new THREE.Euler(0, 0, 0);
}, [props.rotation]);
return (
<group ref={groupRef} position={positionVector} rotation={rotationEuler}>
{/* Balão de Nome acima da cabeça */}
<group position={[0, 0.76, 0]}>
<mesh position={[0, 0, -0.001]}>
<planeGeometry args={[0.26, 0.055]} />
<meshBasicMaterial color="#0f172a" transparent opacity={0.8} />
</mesh>
<Text fontSize={0.026} color="#ffffff" anchorX="center" anchorY="middle" fontWeight="bold">
{username}
</Text>
</group>
{/* Modelo GLB Realista Clonado */}
<primitive object={clone} scale={0.42} position={[0, 0.02, 0]} />
{/* Indicador de Fala & Chat */}
{(isTalking || isLastChatSender) && (
<mesh
ref={ringRef}
rotation={[-Math.PI / 2, 0, 0]}
position={[0, 0.35, 0.02]}
>
<ringGeometry args={[0.075, 0.088, 32]} />
<meshBasicMaterial color="#3b82f6" transparent opacity={0.7} side={THREE.DoubleSide} />
</mesh>
)}
</group>
);
}
// Carregador e Escalador Inteligente da Maquete Holográfica
// Carregador e Escalador Inteligente da Maquete Holográfica
function HologramModel({ model, presentationState }: { model: SceneModel; presentationState: PresentationState }) {