🚀 Auto-deploy: melhoria no snap e medição AR em 25/05/2026 11:15:46
This commit is contained in:
+73
-11
@@ -43,7 +43,7 @@ import { toast } from 'sonner';
|
|||||||
import {
|
import {
|
||||||
Users, MessageSquare, Mic, MicOff, Home, Send,
|
Users, MessageSquare, Mic, MicOff, Home, Send,
|
||||||
ArrowRight, ChevronLeft, ChevronRight, Share2, LogOut,
|
ArrowRight, ChevronLeft, ChevronRight, Share2, LogOut,
|
||||||
RotateCw, Award, ShieldAlert, Maximize2, Minimize2
|
RotateCw, Award, ShieldAlert, Maximize2, Minimize2, Box
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
// Interfaces tipadas para evitar any
|
// Interfaces tipadas para evitar any
|
||||||
@@ -58,9 +58,12 @@ interface PresentationState {
|
|||||||
rotation: number[];
|
rotation: number[];
|
||||||
scale: number;
|
scale: number;
|
||||||
sectionEnabled: boolean;
|
sectionEnabled: boolean;
|
||||||
sectionAxis: string;
|
sectionAxis: 'x' | 'y' | 'z';
|
||||||
sectionLevel: number;
|
sectionLevel: number;
|
||||||
sectionInvert: boolean;
|
sectionInvert: boolean;
|
||||||
|
modelUrl?: string;
|
||||||
|
modelFileName?: string;
|
||||||
|
modelFileSize?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PeerData {
|
interface PeerData {
|
||||||
@@ -716,12 +719,16 @@ export default function MeetingRoom() {
|
|||||||
const [inRoom, setInRoom] = useState(false);
|
const [inRoom, setInRoom] = useState(false);
|
||||||
const [isConnecting, setIsConnecting] = useState(false);
|
const [isConnecting, setIsConnecting] = useState(false);
|
||||||
const [isFocusMode, setIsFocusMode] = useState(false);
|
const [isFocusMode, setIsFocusMode] = useState(false);
|
||||||
const [name, setName] = useState('');
|
const [name, setName] = useState(() => localStorage.getItem('tsxr_meeting_user_name') || '');
|
||||||
const [cargo, setCargo] = useState('');
|
const [cargo, setCargo] = useState(() => localStorage.getItem('tsxr_meeting_user_cargo') || '');
|
||||||
const [empresa, setEmpresa] = useState('');
|
const [empresa, setEmpresa] = useState(() => localStorage.getItem('tsxr_meeting_user_empresa') || '');
|
||||||
const [isPresenter, setIsPresenter] = useState(!roomId); // Se não há roomId na URL, ele é o host
|
const [isPresenter, setIsPresenter] = useState(() => {
|
||||||
const [selectedAvatar, setSelectedAvatar] = useState(1);
|
const saved = localStorage.getItem('tsxr_meeting_user_is_presenter');
|
||||||
const [selectedChair, setSelectedChair] = useState('1');
|
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 || '');
|
const [inputRoomId, setInputRoomId] = useState(roomId || '');
|
||||||
|
|
||||||
// Estados da Sala (Conexão Supabase)
|
// Estados da Sala (Conexão Supabase)
|
||||||
@@ -843,7 +850,17 @@ export default function MeetingRoom() {
|
|||||||
const handlePresentationChange = (updatedFields: Partial<PresentationState>) => {
|
const handlePresentationChange = (updatedFields: Partial<PresentationState>) => {
|
||||||
if (!isPresenter || !channelRef.current) return;
|
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);
|
setPresentationState(newState);
|
||||||
|
|
||||||
channelRef.current.send({
|
channelRef.current.send({
|
||||||
@@ -860,6 +877,14 @@ export default function MeetingRoom() {
|
|||||||
return;
|
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
|
// Validar se o avatar já está em uso por outra pessoa
|
||||||
const avatarInUse = Object.values(peers).find((p) => p.avatarId === selectedAvatar);
|
const avatarInUse = Object.values(peers).find((p) => p.avatarId === selectedAvatar);
|
||||||
if (avatarInUse) {
|
if (avatarInUse) {
|
||||||
@@ -952,7 +977,27 @@ export default function MeetingRoom() {
|
|||||||
// Escuta atualizações de Apresentação da Maquete via Broadcast (apenas para convidados)
|
// Escuta atualizações de Apresentação da Maquete via Broadcast (apenas para convidados)
|
||||||
channel.on('broadcast', { event: 'presentation_update' }, (payload) => {
|
channel.on('broadcast', { event: 'presentation_update' }, (payload) => {
|
||||||
if (!isPresenter) {
|
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
|
// Se for o apresentador, propaga o estado inicial de apresentação (vinda do Viewer) para todos
|
||||||
if (isPresenter) {
|
if (isPresenter) {
|
||||||
|
const store = useModelStore.getState();
|
||||||
|
const currentModel = store.models.find(m => m.id === store.activeModelId) || null;
|
||||||
channel.send({
|
channel.send({
|
||||||
type: 'broadcast',
|
type: 'broadcast',
|
||||||
event: 'presentation_update',
|
event: 'presentation_update',
|
||||||
payload: presentationState
|
payload: {
|
||||||
|
...presentationState,
|
||||||
|
modelUrl: currentModel?.url,
|
||||||
|
modelFileName: currentModel?.fileName,
|
||||||
|
modelFileSize: currentModel?.fileSize
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1389,6 +1441,16 @@ export default function MeetingRoom() {
|
|||||||
>
|
>
|
||||||
<RotateCw className="h-3 w-3" /> Resetar Apresentação
|
<RotateCw className="h-3 w-3" /> Resetar Apresentação
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
{/* Botão de Apresentação no Viewer */}
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
size="sm"
|
||||||
|
className="w-full h-9 font-mono text-[10px] gap-1.5 bg-sky-500 hover:bg-sky-600 text-slate-950 font-bold glow-primary"
|
||||||
|
onClick={() => navigate(`/viewer?room=${roomCode}`)}
|
||||||
|
>
|
||||||
|
<Box className="h-3.5 w-3.5" /> Manipular Peça no Viewer
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
+128
-6
@@ -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 { ArrowLeft, Glasses, Box, Ruler, FileText, Upload, Loader2, Users } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { useModelStore } from "@/stores/useModelStore";
|
import { useModelStore } from "@/stores/useModelStore";
|
||||||
@@ -38,11 +39,120 @@ const Viewer = () => {
|
|||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
const [converting, setConverting] = useState(false);
|
const [converting, setConverting] = useState(false);
|
||||||
|
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const roomId = searchParams.get("room");
|
||||||
|
const channelRef = useRef<any>(null);
|
||||||
|
|
||||||
|
// Se não houver modelo e não estivermos em Reunião, volta para a Home
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!model) {
|
if (!model && !roomId) {
|
||||||
navigate("/");
|
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<HTMLInputElement>) => {
|
const handleFileUpload = useCallback(async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const file = e.target.files?.[0];
|
const file = e.target.files?.[0];
|
||||||
@@ -102,7 +212,7 @@ const Viewer = () => {
|
|||||||
}
|
}
|
||||||
}, [addModel, models.length, maxModels]);
|
}, [addModel, models.length, maxModels]);
|
||||||
|
|
||||||
if (!model) return null;
|
// Removido o bloqueio para permitir carregamento de modelo na sala
|
||||||
|
|
||||||
const handleEnterXR = async () => {
|
const handleEnterXR = async () => {
|
||||||
if (!xrSupported) {
|
if (!xrSupported) {
|
||||||
@@ -152,8 +262,14 @@ const Viewer = () => {
|
|||||||
{/* Top bar */}
|
{/* Top bar */}
|
||||||
<header className="flex items-center justify-between border-b bg-card px-4 py-3">
|
<header className="flex items-center justify-between border-b bg-card px-4 py-3">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Button variant="ghost" size="icon" onClick={() => navigate("/")}>
|
<Button
|
||||||
<ArrowLeft className="h-5 w-5" />
|
variant="ghost"
|
||||||
|
size={roomId ? "default" : "icon"}
|
||||||
|
className={roomId ? "gap-1.5 px-3 text-xs font-mono h-9 hover:bg-slate-800" : ""}
|
||||||
|
onClick={() => roomId ? navigate(`/meeting/${roomId}`) : navigate("/")}
|
||||||
|
>
|
||||||
|
<ArrowLeft className="h-4 w-4" />
|
||||||
|
{roomId && "Voltar para Sala"}
|
||||||
</Button>
|
</Button>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Box className="h-5 w-5 text-primary" />
|
<Box className="h-5 w-5 text-primary" />
|
||||||
@@ -222,11 +338,17 @@ const Viewer = () => {
|
|||||||
<h2 className="mb-2 font-mono text-xs font-semibold uppercase tracking-widest text-muted-foreground">
|
<h2 className="mb-2 font-mono text-xs font-semibold uppercase tracking-widest text-muted-foreground">
|
||||||
Peça selecionada
|
Peça selecionada
|
||||||
</h2>
|
</h2>
|
||||||
|
{model ? (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<InfoItem icon={FileText} label="Arquivo" value={model.fileName} />
|
<InfoItem icon={FileText} label="Arquivo" value={model.fileName} />
|
||||||
<InfoItem icon={Box} label="Tamanho" value={`${(model.fileSize / (1024 * 1024)).toFixed(2)} MB`} />
|
<InfoItem icon={Box} label="Tamanho" value={`${(model.fileSize / (1024 * 1024)).toFixed(2)} MB`} />
|
||||||
<InfoItem icon={Ruler} label="Escala" value={`${scaleRatio.label} (mm)`} />
|
<InfoItem icon={Ruler} label="Escala" value={`${scaleRatio.label} (mm)`} />
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="rounded-lg border border-dashed border-muted p-4 text-center bg-muted/20">
|
||||||
|
<p className="font-mono text-xs text-muted-foreground">Nenhuma peça ativa na cena</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Separator />
|
<Separator />
|
||||||
|
|||||||
Reference in New Issue
Block a user