diff --git a/src/pages/Index.tsx b/src/pages/Index.tsx index 4056262..f1bea15 100644 --- a/src/pages/Index.tsx +++ b/src/pages/Index.tsx @@ -25,7 +25,7 @@ const Index = () => { const { model, addModel, models, maxModels, xrSupported, setXrSupported } = useModelStore(); // --- Painel de Logs Remotos em Tempo Real --- - const [debugLogs, setDebugLogs] = useState<{ level: 'info' | 'warn' | 'error'; message: string; timestamp: string; data?: any }[]>([]); + const [debugLogs, setDebugLogs] = useState<{ level: 'info' | 'warn' | 'error'; message: string; timestamp: string; data?: unknown }[]>([]); const [showLogs, setShowLogs] = useState(() => new URLSearchParams(window.location.search).get('viewlogs') === 'true'); useEffect(() => { @@ -204,6 +204,7 @@ const Index = () => { type="file" accept={ACCEPTED_EXTENSIONS} className="hidden" + title="Importar arquivo de modelo 3D" onChange={handleFileUpload} /> diff --git a/src/pages/MeetingRoom.tsx b/src/pages/MeetingRoom.tsx index ddfd371..8812b37 100644 --- a/src/pages/MeetingRoom.tsx +++ b/src/pages/MeetingRoom.tsx @@ -1,24 +1,48 @@ import { useEffect, useRef, useState, useMemo, Suspense } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; -import { Canvas, useFrame, useThree } from '@react-three/fiber'; +import { Canvas, useFrame } from '@react-three/fiber'; import { OrbitControls, Text, useGLTF } from '@react-three/drei'; import * as THREE from 'three'; import { supabase } from '@/integrations/supabase/client'; import { useModelStore } from '@/stores/useModelStore'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; -import { Label } from '@/components/ui/label'; import { Checkbox } from '@/components/ui/checkbox'; import { toast } from 'sonner'; import { - Users, MessageSquare, Mic, MicOff, Home, Send, Slider, - ArrowRight, ChevronLeft, ChevronRight, Share2, LogOut, Check, - Scissors, RotateCw, Maximize2, ShieldAlert, Award + Users, MessageSquare, Mic, MicOff, Home, Send, + ArrowRight, ChevronLeft, ChevronRight, Share2, LogOut, + RotateCw, Award } from 'lucide-react'; +// Interfaces tipadas para evitar any +interface MiiPreset { + id: number; + name: string; + desc: string; + color: string; +} + +interface PresentationState { + rotation: number[]; + scale: number; + sectionEnabled: boolean; + sectionAxis: string; + sectionLevel: number; + sectionInvert: boolean; +} + +interface PeerData { + username: string; + avatarId: number; + chairId: string; + role: string; + cargo: string; + empresa: string; + presence_ref: string; +} + // --- Configurações da Mesa de Reunião --- -// Coordenadas das 8 cadeiras posicionadas organizadamente FORA da mesa -// A mesa vai de X = -1.9 a 1.9 e Z = -1.1 a 1.1. As cadeiras ficam em Z = -1.45 e Z = 1.45. const CHAIRS = [ { id: '1', name: 'Assento 1', pos: [-1.2, 0.95, -1.45], lookAt: [0, 0.95, 0] }, { id: '2', name: 'Assento 2', pos: [-0.4, 0.95, -1.45], lookAt: [0, 0.95, 0] }, @@ -30,10 +54,9 @@ const CHAIRS = [ { id: '8', name: 'Assento 8', pos: [-1.2, 0.95, 1.45], lookAt: [0, 0.95, 0] }, ]; -// 10 Modelos de Mii com características e cores de cabelo humanas -const AVATAR_PRESETS = [ +const AVATAR_PRESETS: MiiPreset[] = [ { id: 1, name: 'Mii Cyber', desc: 'Cabelo curto preto e óculos vermelhos', color: '#ef4444' }, - { id: 2, name: 'Mii Classic', desc: 'Pele média com cabelo castanho clássico', color: '#b45309' }, + { id: 2, name: 'Mii Classic', desc: 'Pele média com Cabelo castanho clássico', color: '#b45309' }, { id: 3, name: 'Mii Goldie', desc: 'Cabelo loiro longo com olhos azuis expressivos', color: '#eab308' }, { id: 4, name: 'Mii Sporty', desc: 'Boné azul virado para trás e sorriso aberto', color: '#3b82f6' }, { id: 5, name: 'Mii Ginger', desc: 'Cabelo ruivo longo ondulado e expressivo', color: '#f97316' }, @@ -44,16 +67,31 @@ const AVATAR_PRESETS = [ { id: 10, name: 'Mii Straw', desc: 'Chapéu de palha do campo clássico', color: '#ca8a04' }, ]; -// Cores de pele Mii const SKIN_COLORS = ['#fed7aa', '#fdba74', '#f59e0b', '#d97706', '#9a3412', '#ffedd5']; +// Classes Tailwind estáticas para evitar inline styles no list view dos avatares +const getAvatarBgClass = (avatarId: number): string => { + const classes = [ + 'bg-red-500/20 text-red-400', + 'bg-amber-800/20 text-amber-500', + 'bg-yellow-500/20 text-yellow-400', + 'bg-blue-500/20 text-blue-400', + 'bg-orange-500/20 text-orange-400', + 'bg-emerald-500/20 text-emerald-400', + 'bg-amber-900/20 text-amber-600', + 'bg-slate-700/20 text-slate-400', + 'bg-purple-500/20 text-purple-400', + 'bg-yellow-600/20 text-yellow-500' + ]; + return classes[(avatarId - 1) % 10] || classes[0]; +}; + // --- Subcomponentes 3D --- // Avatar estilo Mii (Nintendo Wii) -function MiiAvatar({ avatarId, username, isTalking, ...props }: { avatarId: number; username: string; isTalking?: boolean } & any) { +function MiiAvatar({ avatarId, username, isTalking, ...props }: { avatarId: number; username: string; isTalking?: boolean } & Record) { const groupRef = useRef(null); - // Parâmetros do rosto e cabelo com base no ID const mii = useMemo(() => { const skin = SKIN_COLORS[(avatarId - 1) % SKIN_COLORS.length]; return { @@ -71,20 +109,34 @@ function MiiAvatar({ avatarId, username, isTalking, ...props }: { avatarId: numb }, [avatarId]); useFrame((state) => { - if (groupRef.current) { - groupRef.current.position.y = props.position[1] + Math.sin(state.clock.getElapsedTime() * 2 + avatarId) * 0.012; + 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; } }); + const positionVector = useMemo(() => { + if (props.position && Array.isArray(props.position)) { + return new THREE.Vector3(props.position[0] as number, props.position[1] as number, props.position[2] as number); + } + return new THREE.Vector3(0, 0, 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 ( - + {/* Balão de Nome acima da cabeça */} - + {username} @@ -138,7 +190,7 @@ function MiiAvatar({ avatarId, username, isTalking, ...props }: { avatarId: numb - {/* Aba do Boné (virada para trás) */} + {/* Aba do Boné */} @@ -197,12 +249,10 @@ function MiiAvatar({ avatarId, username, isTalking, ...props }: { avatarId: numb {/* BARBA E BIGODE */} {mii.beard && ( - {/* Bigode */} - {/* Barbicha */} @@ -213,17 +263,14 @@ function MiiAvatar({ avatarId, username, isTalking, ...props }: { avatarId: numb {/* ÓCULOS */} {mii.hasGlasses && ( - {/* Lente esquerda */} - {/* Lente direita */} - {/* Ponte */} @@ -235,8 +282,7 @@ function MiiAvatar({ avatarId, username, isTalking, ...props }: { avatarId: numb } // Carregador e Escalador Inteligente da Maquete Holográfica -// Sincroniza transformações de rotação, escala e planos de corte do Apresentador -function HologramModel({ model, presentationState }: { model: { url: string }; presentationState: any }) { +function HologramModel({ model, presentationState }: { model: { url: string }; presentationState: PresentationState }) { const { scene } = useGLTF(model.url); const clone = useMemo(() => scene.clone(), [scene]); const groupRef = useRef(null); @@ -270,7 +316,6 @@ function HologramModel({ model, presentationState }: { model: { url: string }; p groupRef.current.rotation.z = rotation[2]; } if (scale) { - // Multiplica a escala base inteligente pelo fator da apresentação const box = new THREE.Box3().setFromObject(clone); const size = new THREE.Vector3(); box.getSize(size); @@ -303,13 +348,11 @@ function HologramModel({ model, presentationState }: { model: { url: string }; p return; } - // Vetor normal baseado no eixo e na inversão do corte const normal = new THREE.Vector3(); if (sectionAxis === 'x') normal.set(sectionInvert ? -1 : 1, 0, 0); else if (sectionAxis === 'y') normal.set(0, sectionInvert ? -1 : 1, 0); else if (sectionAxis === 'z') normal.set(0, 0, sectionInvert ? -1 : 1); - // O plano corta na coordenada desejada const plane = new THREE.Plane(normal, sectionLevel); clone.traverse((obj) => { @@ -335,7 +378,7 @@ function HologramModel({ model, presentationState }: { model: { url: string }; p } // Cenário da Sala de Reunião Clara -function ThreeMeetingRoomScene({ currentActiveModel, presentationState }: { currentActiveModel: any; presentationState: any }) { +function ThreeMeetingRoomScene({ currentActiveModel, presentationState }: { currentActiveModel: { url: string; fileName: string } | null; presentationState: PresentationState }) { const tableGeom = useMemo(() => { // Geometria da mesa em U const shape = new THREE.Shape(); @@ -471,7 +514,7 @@ function ThreeMeetingRoomScene({ currentActiveModel, presentationState }: { curr ))} {/* Iluminação Clara da Sala */} - + @@ -497,7 +540,7 @@ export default function MeetingRoom() { const [inputRoomId, setInputRoomId] = useState(roomId || ''); // Estados da Sala - const [peers, setPeers] = useState>({}); + const [peers, setPeers] = useState>({}); const [chatMessages, setChatMessages] = useState<{ id: string; sender: string; text: string; time: string }[]>([]); const [newMessage, setNewMessage] = useState(''); const [isMuted, setIsMuted] = useState(false); @@ -507,7 +550,7 @@ export default function MeetingRoom() { const [activeTab, setActiveTab] = useState<'chat' | 'participants'>('participants'); // Estado compartilhado da Apresentação da maquete - const [presentationState, setPresentationState] = useState({ + const [presentationState, setPresentationState] = useState({ rotation: [0, 0, 0], scale: 1, sectionEnabled: false, @@ -516,7 +559,7 @@ export default function MeetingRoom() { sectionInvert: false }); - const channelRef = useRef(null); + const channelRef = useRef | null>(null); const chatScrollRef = useRef(null); // Se o roomId mudar na url, atualiza o checkbox isPresenter @@ -544,10 +587,10 @@ export default function MeetingRoom() { }, [chatMessages]); // Envia as atualizações de apresentação via Broadcast para a sala inteira - const handlePresentationChange = (updatedFields: Partial) => { + const handlePresentationChange = (updatedFields: Partial) => { if (!isPresenter || !channelRef.current) return; - const newState = { ...presentationState, ...updatedFields }; + const newState = { ...presentationState, ...updatedFields } as PresentationState; setPresentationState(newState); channelRef.current.send({ @@ -581,11 +624,11 @@ export default function MeetingRoom() { channel .on('presence', { event: 'sync' }, () => { const state = channel.presenceState(); - const formattedPeers: Record = {}; + const formattedPeers: Record = {}; let count = 0; Object.keys(state).forEach((chairKey) => { - const presences = state[chairKey] as any[]; + const presences = state[chairKey] as unknown as PeerData[]; if (presences && presences.length > 0) { formattedPeers[chairKey] = presences[0]; count++; @@ -595,10 +638,10 @@ export default function MeetingRoom() { setActiveUsersCount(count); }) .on('presence', { event: 'join' }, ({ key, newPresences }) => { - toast.info(`"${newPresences[0]?.username}" sentou-se no assento ${key}`); + toast.info(`"${(newPresences[0] as unknown as PeerData)?.username}" sentou-se no assento ${key}`); }) .on('presence', { event: 'leave' }, ({ key, leftPresences }) => { - toast.info(`"${leftPresences[0]?.username}" liberou o assento ${key}`); + toast.info(`"${(leftPresences[0] as unknown as PeerData)?.username}" liberou o assento ${key}`); }); // Escuta mensagens do Chat via Broadcast @@ -617,7 +660,7 @@ export default function MeetingRoom() { // Escuta atualizações de Apresentação da Maquete via Broadcast (apenas para convidados) channel.on('broadcast', { event: 'presentation_update' }, (payload) => { if (!isPresenter) { - setPresentationState(payload.payload); + setPresentationState(payload.payload as PresentationState); } }); @@ -886,7 +929,7 @@ export default function MeetingRoom() { Painel do Apresentador - Sincronizado + Sincronizado {/* Rotação Y */} @@ -900,6 +943,7 @@ export default function MeetingRoom() { min={-Math.PI} max={Math.PI} step={0.05} + title="Giro Horizontal" value={presentationState.rotation[1]} onChange={(e) => handlePresentationChange({ rotation: [0, parseFloat(e.target.value), 0] })} className="w-full h-1 bg-slate-800 rounded-lg appearance-none cursor-pointer accent-primary" @@ -917,6 +961,7 @@ export default function MeetingRoom() { min={0.3} max={3.0} step={0.1} + title="Zoom da Peça" value={presentationState.scale} onChange={(e) => handlePresentationChange({ scale: parseFloat(e.target.value) })} className="w-full h-1 bg-slate-800 rounded-lg appearance-none cursor-pointer accent-primary" @@ -930,6 +975,7 @@ export default function MeetingRoom() { handlePresentationChange({ sectionEnabled: e.target.checked })} className="rounded border-slate-800 bg-slate-950 accent-primary cursor-pointer h-3.5 w-3.5" @@ -964,6 +1010,7 @@ export default function MeetingRoom() { min={-0.5} max={0.5} step={0.01} + title="Nível de Corte" value={presentationState.sectionLevel} onChange={(e) => handlePresentationChange({ sectionLevel: parseFloat(e.target.value) })} className="w-full h-1 bg-slate-800 rounded-lg appearance-none cursor-pointer accent-primary" @@ -978,6 +1025,7 @@ export default function MeetingRoom() { handlePresentationChange({ sectionInvert: e.target.checked })} className="rounded border-slate-800 bg-slate-950 accent-primary cursor-pointer h-3 w-3" @@ -1102,13 +1150,12 @@ export default function MeetingRoom() { {Object.keys(peers).map((chairKey) => { const peer = peers[chairKey]; const isHost = peer.role === 'presenter'; - const avatar = AVATAR_PRESETS[peer.avatarId - 1] || AVATAR_PRESETS[0]; const isMe = peer.chairId === selectedChair; return (
- {/* Indicador da cor do avatar do Mii */} -
+ {/* Indicador da cor do avatar do Mii via classe estática Tailwind */} +
{peer.username.substring(0, 2).toUpperCase()}