diff --git a/src/pages/MeetingRoom.tsx b/src/pages/MeetingRoom.tsx index 0478a02..b684240 100644 --- a/src/pages/MeetingRoom.tsx +++ b/src/pages/MeetingRoom.tsx @@ -43,7 +43,7 @@ import { toast } from 'sonner'; import { Users, MessageSquare, Mic, MicOff, Home, Send, ArrowRight, ChevronLeft, ChevronRight, Share2, LogOut, - RotateCw, Award, ShieldAlert, Maximize2, Minimize2 + RotateCw, Award, ShieldAlert, Maximize2, Minimize2, Box } from 'lucide-react'; // Interfaces tipadas para evitar any @@ -58,9 +58,12 @@ interface PresentationState { rotation: number[]; scale: number; sectionEnabled: boolean; - sectionAxis: string; + sectionAxis: 'x' | 'y' | 'z'; sectionLevel: number; sectionInvert: boolean; + modelUrl?: string; + modelFileName?: string; + modelFileSize?: number; } interface PeerData { @@ -716,12 +719,16 @@ export default function MeetingRoom() { const [inRoom, setInRoom] = useState(false); const [isConnecting, setIsConnecting] = useState(false); const [isFocusMode, setIsFocusMode] = useState(false); - const [name, setName] = useState(''); - const [cargo, setCargo] = useState(''); - const [empresa, setEmpresa] = useState(''); - const [isPresenter, setIsPresenter] = useState(!roomId); // Se não há roomId na URL, ele é o host - const [selectedAvatar, setSelectedAvatar] = useState(1); - const [selectedChair, setSelectedChair] = useState('1'); + const [name, setName] = useState(() => localStorage.getItem('tsxr_meeting_user_name') || ''); + const [cargo, setCargo] = useState(() => localStorage.getItem('tsxr_meeting_user_cargo') || ''); + const [empresa, setEmpresa] = useState(() => localStorage.getItem('tsxr_meeting_user_empresa') || ''); + const [isPresenter, setIsPresenter] = useState(() => { + const saved = localStorage.getItem('tsxr_meeting_user_is_presenter'); + if (saved !== null) return saved === 'true'; + return !roomId; + }); + const [selectedAvatar, setSelectedAvatar] = useState(() => Number(localStorage.getItem('tsxr_meeting_user_avatar')) || 1); + const [selectedChair, setSelectedChair] = useState(() => localStorage.getItem('tsxr_meeting_user_chair') || '1'); const [inputRoomId, setInputRoomId] = useState(roomId || ''); // Estados da Sala (Conexão Supabase) @@ -843,7 +850,17 @@ export default function MeetingRoom() { const handlePresentationChange = (updatedFields: Partial) => { if (!isPresenter || !channelRef.current) return; - const newState = { ...presentationState, ...updatedFields } as PresentationState; + const store = useModelStore.getState(); + const currentModel = store.models.find(m => m.id === store.activeModelId) || null; + + const newState = { + ...presentationState, + ...updatedFields, + modelUrl: currentModel?.url, + modelFileName: currentModel?.fileName, + modelFileSize: currentModel?.fileSize + } as PresentationState; + setPresentationState(newState); channelRef.current.send({ @@ -859,6 +876,14 @@ export default function MeetingRoom() { toast.error('Digite seu nome primeiro'); return; } + + // Salva no localStorage para auto-preenchimento conveniente + localStorage.setItem('tsxr_meeting_user_name', name); + localStorage.setItem('tsxr_meeting_user_cargo', cargo); + localStorage.setItem('tsxr_meeting_user_empresa', empresa); + localStorage.setItem('tsxr_meeting_user_avatar', String(selectedAvatar)); + localStorage.setItem('tsxr_meeting_user_chair', selectedChair); + localStorage.setItem('tsxr_meeting_user_is_presenter', String(isPresenter)); // Validar se o avatar já está em uso por outra pessoa const avatarInUse = Object.values(peers).find((p) => p.avatarId === selectedAvatar); @@ -952,7 +977,27 @@ 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 as PresentationState); + const data = payload.payload as PresentationState; + setPresentationState(data); + + // Se o apresentador propagar um modelo remoto que o convidado não tem ativo, carrega + if (data.modelUrl && data.modelFileName) { + const store = useModelStore.getState(); + const hasModel = store.models.some(m => m.fileName === data.modelFileName); + if (!hasModel) { + console.log(`📥 [MeetingRoom] Carregando modelo remoto do apresentador: ${data.modelFileName}`); + useModelStore.getState().addModel({ + fileName: data.modelFileName, + fileSize: data.modelFileSize || 0, + url: data.modelUrl + }); + } else { + const existing = store.models.find(m => m.fileName === data.modelFileName); + if (existing && store.activeModelId !== existing.id) { + useModelStore.getState().setActiveModel(existing.id); + } + } + } } }); @@ -972,10 +1017,17 @@ export default function MeetingRoom() { // Se for o apresentador, propaga o estado inicial de apresentação (vinda do Viewer) para todos if (isPresenter) { + const store = useModelStore.getState(); + const currentModel = store.models.find(m => m.id === store.activeModelId) || null; channel.send({ type: 'broadcast', event: 'presentation_update', - payload: presentationState + payload: { + ...presentationState, + modelUrl: currentModel?.url, + modelFileName: currentModel?.fileName, + modelFileSize: currentModel?.fileSize + } }); } @@ -1389,6 +1441,16 @@ export default function MeetingRoom() { > Resetar Apresentação + + {/* Botão de Apresentação no Viewer */} + )} diff --git a/src/pages/Viewer.tsx b/src/pages/Viewer.tsx index a20eb74..c451a2a 100644 --- a/src/pages/Viewer.tsx +++ b/src/pages/Viewer.tsx @@ -1,4 +1,5 @@ -import { useNavigate } from "react-router-dom"; +import { useNavigate, useSearchParams } from "react-router-dom"; +import { supabase } from "@/integrations/supabase/client"; import { ArrowLeft, Glasses, Box, Ruler, FileText, Upload, Loader2, Users } from "lucide-react"; import { Button } from "@/components/ui/button"; import { useModelStore } from "@/stores/useModelStore"; @@ -38,11 +39,120 @@ const Viewer = () => { const fileInputRef = useRef(null); const [converting, setConverting] = useState(false); + const [searchParams] = useSearchParams(); + const roomId = searchParams.get("room"); + const channelRef = useRef(null); + + // Se não houver modelo e não estivermos em Reunião, volta para a Home useEffect(() => { - if (!model) { + if (!model && !roomId) { navigate("/"); } - }, [model, navigate]); + }, [model, roomId, navigate]); + + const sendMeetingUpdate = useCallback((ft: typeof fineTuning, secEn: typeof sectionEnabled, secLevel: typeof sectionLevel, secInv: typeof sectionInvert) => { + if (!roomId || !channelRef.current) return; + + const enabledAxis = (['x', 'y', 'z'] as const).find(ax => secEn[ax]); + const isSectionActive = !!enabledAxis; + const sectionAxis = enabledAxis || 'y'; + const sectionLevelVal = isSectionActive ? secLevel[sectionAxis] : 0.0; + const sectionInvertVal = isSectionActive ? secInv[sectionAxis] : false; + + // Obtém dados da maquete ativa na store + const activeModelStore = useModelStore.getState().model; + + const payload = { + rotation: [ft.rotX, ft.rotY, ft.rotZ], + scale: ft.scale, + sectionEnabled: isSectionActive, + sectionAxis, + sectionLevel: sectionLevelVal, + sectionInvert: sectionInvertVal, + modelUrl: activeModelStore?.url, + modelFileName: activeModelStore?.fileName, + modelFileSize: activeModelStore?.fileSize + }; + + console.log("📡 [Viewer Meeting Sync] Enviando atualização de apresentação:", payload); + channelRef.current.send({ + type: 'broadcast', + event: 'presentation_update', + payload + }); + }, [roomId]); + + // Conecta ao canal do Supabase se o Viewer foi aberto no modo Reunião + useEffect(() => { + if (!roomId) return; + + const finalRoomId = roomId.trim().toUpperCase(); + console.log(`🔌 [Viewer Meeting Sync] Conectando ao canal da reunião: ${finalRoomId}`); + + const channel = supabase.channel(`meeting_room_${finalRoomId}`, { + config: { + broadcast: { self: false } + } + }); + + channelRef.current = channel; + + channel.subscribe((status) => { + console.log(`🔌 [Viewer Meeting Sync] Status do canal: ${status}`); + if (status === 'SUBSCRIBED') { + const state = useModelStore.getState(); + sendMeetingUpdate(state.fineTuning, state.sectionEnabled, state.sectionLevel, state.sectionInvert); + } + }); + + return () => { + console.log(`🔌 [Viewer Meeting Sync] Desconectando do canal da reunião: ${finalRoomId}`); + channel.unsubscribe(); + supabase.removeChannel(channel); + }; + }, [roomId, sendMeetingUpdate]); + + // Observa mudanças primitivas nas ferramentas 3D para propagação imediata via Broadcast + const rotX = fineTuning.rotX; + const rotY = fineTuning.rotY; + const rotZ = fineTuning.rotZ; + const scaleVal = fineTuning.scale; + + const secEnabledX = sectionEnabled.x; + const secEnabledY = sectionEnabled.y; + const secEnabledZ = sectionEnabled.z; + + const secLevelX = sectionLevel.x; + const secLevelY = sectionLevel.y; + const secLevelZ = sectionLevel.z; + + const secInvertX = sectionInvert.x; + const secInvertY = sectionInvert.y; + const secInvertZ = sectionInvert.z; + + const activeModelFileName = model?.fileName; + + useEffect(() => { + if (!roomId || !channelRef.current) return; + sendMeetingUpdate(fineTuning, sectionEnabled, sectionLevel, sectionInvert); + }, [ + roomId, + rotX, + rotY, + rotZ, + scaleVal, + secEnabledX, + secEnabledY, + secEnabledZ, + secLevelX, + secLevelY, + secLevelZ, + secInvertX, + secInvertY, + secInvertZ, + activeModelFileName, + sendMeetingUpdate + ]); const handleFileUpload = useCallback(async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; @@ -102,7 +212,7 @@ const Viewer = () => { } }, [addModel, models.length, maxModels]); - if (!model) return null; + // Removido o bloqueio para permitir carregamento de modelo na sala const handleEnterXR = async () => { if (!xrSupported) { @@ -152,8 +262,14 @@ const Viewer = () => { {/* Top bar */}
-
@@ -222,11 +338,17 @@ const Viewer = () => {

Peça selecionada

-
- - - -
+ {model ? ( +
+ + + +
+ ) : ( +
+

Nenhuma peça ativa na cena

+
+ )}