🚀 Auto-deploy: melhoria no snap e medição AR em 25/05/2026 01:10:53

This commit is contained in:
2026-05-25 01:10:53 +00:00
parent c16beadb32
commit 408eba84e0
+153 -140
View File
@@ -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 RotateCw, Award, ShieldAlert, Maximize2, Minimize2
} from 'lucide-react'; } from 'lucide-react';
// Interfaces tipadas para evitar any // Interfaces tipadas para evitar any
@@ -381,10 +381,21 @@ function HologramModel({ model, presentationState }: { model: { url: string }; p
const { scene } = useGLTF(model.url); const { scene } = useGLTF(model.url);
const clone = useMemo(() => scene.clone(), [scene]); const clone = useMemo(() => scene.clone(), [scene]);
const groupRef = useRef<THREE.Group>(null); const groupRef = useRef<THREE.Group>(null);
const baseScaleRef = useRef<number>(1);
const centerOffsetRef = useRef<THREE.Vector3>(new THREE.Vector3());
// Calcula escala de aproximadamente 1 metro para caber na mesa // Calcula escala de aproximadamente 1 metro para caber na mesa (apenas UMA vez no carregamento)
useEffect(() => { useEffect(() => {
if (!groupRef.current) return; if (!groupRef.current) return;
// Salva transformações antigas
const oldRot = clone.rotation.clone();
const oldScale = clone.scale.clone();
// Reseta temporariamente para obter o bounding box original
clone.rotation.set(0, 0, 0);
clone.scale.setScalar(1);
const box = new THREE.Box3().setFromObject(clone); const box = new THREE.Box3().setFromObject(clone);
const size = new THREE.Vector3(); const size = new THREE.Vector3();
box.getSize(size); box.getSize(size);
@@ -393,12 +404,19 @@ function HologramModel({ model, presentationState }: { model: { url: string }; p
if (maxDim > 0) { if (maxDim > 0) {
const targetSize = 0.95; const targetSize = 0.95;
const scaleFactor = targetSize / maxDim; const scaleFactor = targetSize / maxDim;
groupRef.current.scale.setScalar(scaleFactor); baseScaleRef.current = scaleFactor;
const center = new THREE.Vector3(); const center = new THREE.Vector3();
box.getCenter(center); box.getCenter(center);
clone.position.copy(center).multiplyScalar(-scaleFactor); centerOffsetRef.current.copy(center).multiplyScalar(-scaleFactor);
clone.position.y += 0.02; centerOffsetRef.current.y += 0.02;
clone.position.copy(centerOffsetRef.current);
} }
// Restaura
clone.rotation.copy(oldRot);
clone.scale.copy(oldScale);
}, [clone]); }, [clone]);
// Aplica transformações de Rotação e Escala compartilhadas // Aplica transformações de Rotação e Escala compartilhadas
@@ -411,14 +429,7 @@ function HologramModel({ model, presentationState }: { model: { url: string }; p
groupRef.current.rotation.z = rotation[2]; groupRef.current.rotation.z = rotation[2];
} }
if (scale) { if (scale) {
const box = new THREE.Box3().setFromObject(clone); groupRef.current.scale.setScalar(baseScaleRef.current * scale);
const size = new THREE.Vector3();
box.getSize(size);
const maxDim = Math.max(size.x, size.y, size.z);
if (maxDim > 0) {
const baseScale = 0.95 / maxDim;
groupRef.current.scale.setScalar(baseScale * scale);
}
} }
} }
}); });
@@ -473,131 +484,114 @@ function HologramModel({ model, presentationState }: { model: { url: string }; p
} }
// Cenário da Sala de Reunião Clara // Cenário da Sala de Reunião Clara
function ThreeMeetingRoomScene({ currentActiveModel, presentationState }: { currentActiveModel: { url: string; fileName: string } | null; presentationState: PresentationState }) { function ThreeMeetingRoomScene({ currentActiveModel, presentationState, isFocusMode }: { currentActiveModel: { url: string; fileName: string } | null; presentationState: PresentationState; isFocusMode: boolean }) {
const tableGeom = useMemo(() => {
// Geometria da mesa em U
const shape = new THREE.Shape();
shape.moveTo(-1.6, -1.1);
shape.lineTo(1.6, -1.1);
shape.quadraticCurveTo(1.9, -1.1, 1.9, -0.8);
shape.lineTo(1.9, 0.8);
shape.quadraticCurveTo(1.9, 1.1, 1.6, 1.1);
shape.lineTo(-1.6, 1.1);
shape.quadraticCurveTo(-1.9, 1.1, -1.9, 0.8);
shape.lineTo(-1.9, -0.8);
shape.quadraticCurveTo(-1.9, -1.1, -1.6, -1.1);
const hole = new THREE.Path();
hole.absellipse(0, 0, 0.9, 0.45, 0, Math.PI * 2, true);
shape.holes.push(hole);
return new THREE.ExtrudeGeometry(shape, { depth: 0.06, bevelEnabled: true, bevelThickness: 0.01, bevelSize: 0.01, bevelSegments: 3 });
}, []);
return ( return (
<group> <group>
{/* Piso Clara */} {/* Luzes da Sala / Foco */}
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, 0, 0]}> <ambientLight intensity={isFocusMode ? 0.7 : 0.8} />
<planeGeometry args={[12, 8]} /> <directionalLight position={isFocusMode ? [5, 5, 5] : [4, 6, 4]} intensity={1.5} castShadow={!isFocusMode} />
<meshStandardMaterial color="#e2e8f0" roughness={0.4} metalness={0.1} />
</mesh>
{/* Tapete Central */} {isFocusMode && (
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, 0.001, 0]}> <color attach="background" args={['#090d16']} />
<planeGeometry args={[4.8, 3.2]} />
<meshStandardMaterial color="#94a3b8" roughness={0.7} />
</mesh>
{/* Paredes Claras */}
<mesh position={[0, 2, -4]}>
<planeGeometry args={[12, 4]} />
<meshStandardMaterial color="#f1f5f9" roughness={0.9} />
</mesh>
{/* Janelas Panorâmicas */}
<mesh position={[-6, 2, 0]} rotation={[0, Math.PI / 2, 0]}>
<planeGeometry args={[8, 4]} />
<meshStandardMaterial color="#bae6fd" roughness={0.1} emissive="#bae6fd" emissiveIntensity={0.4} />
</mesh>
<mesh position={[6, 2, 0]} rotation={[0, -Math.PI / 2, 0]}>
<planeGeometry args={[8, 4]} />
<meshStandardMaterial color="#bae6fd" roughness={0.1} emissive="#bae6fd" emissiveIntensity={0.4} />
</mesh>
{/* Mesa Elíptica em U (Madeira Mel Carvalho Clara) */}
<group position={[0, 0.8, 0]} rotation={[Math.PI / 2, 0, 0]}>
<mesh geometry={tableGeom}>
<meshStandardMaterial color="#d97706" roughness={0.2} metalness={0.1} />
</mesh>
</group>
{/* Pés da mesa (Cromados) */}
{[-1.3, 1.3].map((x) =>
[-0.7, 0.7].map((z) => (
<mesh key={`${x}-${z}`} position={[x, 0.4, z]}>
<cylinderGeometry args={[0.035, 0.035, 0.8]} />
<meshStandardMaterial color="#d4d4d8" metalness={0.9} roughness={0.1} />
</mesh>
))
)} )}
{/* Projetor de Holograma (Centro da mesa) */} {!isFocusMode && (
<group position={[0, 0.78, 0]}> <>
<mesh rotation={[-Math.PI / 2, 0, 0]}> {/* Piso Clara */}
<cylinderGeometry args={[0.26, 0.28, 0.05, 32]} /> <mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, 0, 0]}>
<meshStandardMaterial color="#1e293b" metalness={0.8} roughness={0.2} /> <planeGeometry args={[12, 8]} />
</mesh> <meshStandardMaterial color="#e2e8f0" roughness={0.4} metalness={0.1} />
<mesh position={[0, 0.026, 0]} rotation={[-Math.PI / 2, 0, 0]}> </mesh>
<ringGeometry args={[0, 0.22, 32]} />
<meshBasicMaterial color="#0ea5e9" transparent opacity={0.7} /> {/* Tapete Central */}
</mesh> <mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, 0.001, 0]}>
<mesh position={[0, 0.45, 0]}> <planeGeometry args={[4.8, 3.2]} />
<cylinderGeometry args={[0.28, 0.2, 0.9, 32, 1, true]} /> <meshStandardMaterial color="#94a3b8" roughness={0.7} />
<meshBasicMaterial color="#0ea5e9" transparent opacity={0.15} side={THREE.DoubleSide} /> </mesh>
</mesh>
{/* Paredes Claras */}
{/* Modelo 3D da Maquete Holográfica */} <mesh position={[0, 2, -4]}>
<group position={[0, 0.45, 0]}> <planeGeometry args={[12, 4]} />
<ModelErrorBoundary fallback={ <meshStandardMaterial color="#f1f5f9" roughness={0.9} />
// Se falhar ao carregar o modelo (ex: blob local do host inacessível por convidados), renderiza o holograma geométrico </mesh>
<group rotation={[0, Date.now() * 0.00012, 0]}> {/* Janelas Panorâmicas */}
<mesh position={[0, 0, 0]}> <mesh position={[-6, 2, 0]} rotation={[0, Math.PI / 2, 0]}>
<octahedronGeometry args={[0.22]} /> <planeGeometry args={[8, 4]} />
<meshStandardMaterial color="#f43f5e" transparent opacity={0.7} wireframe emissive="#f43f5e" emissiveIntensity={0.5} /> <meshStandardMaterial color="#bae6fd" roughness={0.1} emissive="#bae6fd" emissiveIntensity={0.4} />
</mesh> </mesh>
<mesh position={[0, -0.15, 0]}> <mesh position={[6, 2, 0]} rotation={[0, -Math.PI / 2, 0]}>
<boxGeometry args={[0.3, 0.02, 0.3]} /> <planeGeometry args={[8, 4]} />
<meshStandardMaterial color="#ef4444" transparent opacity={0.4} /> <meshStandardMaterial color="#bae6fd" roughness={0.1} emissive="#bae6fd" emissiveIntensity={0.4} />
</mesh> </mesh>
</group>
{/* Mesa Cilíndrica Central de 1m de diâmetro (raio 0.5) */}
<group position={[0, 0, 0]}>
{/* Base no chão */}
<mesh position={[0, 0.01, 0]} castShadow receiveShadow>
<cylinderGeometry args={[0.25, 0.25, 0.02, 32]} />
<meshStandardMaterial color="#475569" metalness={0.7} roughness={0.2} />
</mesh>
{/* Coluna / Pé da mesa */}
<mesh position={[0, 0.45, 0]} castShadow receiveShadow>
<cylinderGeometry args={[0.06, 0.06, 0.9, 32]} />
<meshStandardMaterial color="#64748b" metalness={0.8} roughness={0.1} />
</mesh>
{/* Tampo da mesa (Diâmetro 1m, altura 0.9m) */}
<mesh position={[0, 0.9, 0]} castShadow receiveShadow>
<cylinderGeometry args={[0.5, 0.5, 0.06, 48]} />
<meshStandardMaterial color="#d97706" roughness={0.4} metalness={0.1} />
</mesh>
{/* Projetor de Holograma (Centro da mesa) */}
<mesh position={[0, 0.931, 0]} rotation={[-Math.PI / 2, 0, 0]}>
<ringGeometry args={[0.22, 0.25, 32]} />
<meshBasicMaterial color="#0ea5e9" transparent opacity={0.6} />
</mesh>
</group>
</>
)}
{/* Modelo 3D da Maquete Holográfica */}
<group position={isFocusMode ? [0, 0, 0] : [0, 1.23, 0]}>
<ModelErrorBoundary fallback={
// Se falhar ao carregar o modelo (ex: blob local do host inacessível por convidados), renderiza o holograma geométrico
<group rotation={[0, Date.now() * 0.00012, 0]}>
<mesh position={[0, 0, 0]}>
<octahedronGeometry args={[0.22]} />
<meshStandardMaterial color="#f43f5e" transparent opacity={0.7} wireframe emissive="#f43f5e" emissiveIntensity={0.5} />
</mesh>
<mesh position={[0, -0.15, 0]}>
<boxGeometry args={[0.3, 0.02, 0.3]} />
<meshStandardMaterial color="#ef4444" transparent opacity={0.4} />
</mesh>
</group>
}>
<Suspense fallback={
<mesh>
<sphereGeometry args={[0.1, 16, 16]} />
<meshBasicMaterial color="#00f5ff" wireframe />
</mesh>
}> }>
<Suspense fallback={ {currentActiveModel ? (
<mesh> <HologramModel model={currentActiveModel} presentationState={presentationState} />
<sphereGeometry args={[0.1, 16, 16]} /> ) : (
<meshBasicMaterial color="#00f5ff" wireframe /> // Holograma Padrão Geométrico se não houver maquete carregada
</mesh> <group rotation={[0, Date.now() * 0.00012, 0]}>
}> <mesh position={[0, 0, 0]}>
{currentActiveModel ? ( <octahedronGeometry args={[0.22]} />
<HologramModel model={currentActiveModel} presentationState={presentationState} /> <meshStandardMaterial color="#0ea5e9" transparent opacity={0.7} wireframe emissive="#0ea5e9" emissiveIntensity={0.5} />
) : ( </mesh>
// Holograma Padrão Geométrico se não houver maquete carregada <mesh position={[0, 0, 0]} rotation={[0, Math.PI / 4, 0]}>
<group rotation={[0, Date.now() * 0.00012, 0]}> <boxGeometry args={[0.25, 0.04, 0.25]} />
<mesh position={[0, 0, 0]}> <meshStandardMaterial color="#3b82f6" transparent opacity={0.4} emissive="#3b82f6" emissiveIntensity={0.2} />
<octahedronGeometry args={[0.22]} /> </mesh>
<meshStandardMaterial color="#0ea5e9" transparent opacity={0.7} wireframe emissive="#0ea5e9" emissiveIntensity={0.5} /> </group>
</mesh> )}
<mesh position={[0, 0, 0]} rotation={[0, Math.PI / 4, 0]}> </Suspense>
<boxGeometry args={[0.25, 0.04, 0.25]} /> </ModelErrorBoundary>
<meshStandardMaterial color="#3b82f6" transparent opacity={0.4} emissive="#3b82f6" emissiveIntensity={0.2} />
</mesh>
</group>
)}
</Suspense>
</ModelErrorBoundary>
</group>
</group> </group>
{/* Renderizar as Cadeiras Físicas da mesa */} {/* Renderizar as Cadeiras Físicas da mesa - Ocultadas no Modo Foco */}
{CHAIRS.map((chair) => ( {!isFocusMode && CHAIRS.map((chair) => (
<group key={chair.id} position={[chair.pos[0], 0, chair.pos[2]]} rotation={[0, Math.atan2(chair.lookAt[0] - chair.pos[0], chair.lookAt[2] - chair.pos[2]), 0]}> <group key={chair.id} position={[chair.pos[0], 0, chair.pos[2]]} rotation={[0, Math.atan2(chair.lookAt[0] - chair.pos[0], chair.lookAt[2] - chair.pos[2]), 0]}>
{/* Base giratória metálica */} {/* Base giratória metálica */}
<mesh position={[0, 0.25, 0]}> <mesh position={[0, 0.25, 0]}>
@@ -622,11 +616,15 @@ function ThreeMeetingRoomScene({ currentActiveModel, presentationState }: { curr
</group> </group>
))} ))}
{/* Iluminação Clara da Sala */} {/* Iluminação Clara da Sala - Ocultada no Modo Foco */}
<hemisphereLight color="#ffffff" groundColor="#64748b" intensity={1.4} /> {!isFocusMode && (
<directionalLight position={[6, 12, 6]} intensity={1.8} castShadow /> <>
<directionalLight position={[-6, 8, -6]} intensity={0.5} /> <hemisphereLight color="#ffffff" groundColor="#64748b" intensity={1.4} />
<pointLight position={[0, 3.5, 0]} intensity={2.0} color="#0ea5e9" distance={8} /> <directionalLight position={[6, 12, 6]} intensity={1.8} castShadow />
<directionalLight position={[-6, 8, -6]} intensity={0.5} />
<pointLight position={[0, 3.5, 0]} intensity={2.0} color="#0ea5e9" distance={8} />
</>
)}
</group> </group>
); );
} }
@@ -641,6 +639,7 @@ export default function MeetingRoom() {
// Estados do Lobby // Estados do Lobby
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 [name, setName] = useState(''); const [name, setName] = useState('');
const [cargo, setCargo] = useState(''); const [cargo, setCargo] = useState('');
const [empresa, setEmpresa] = useState(''); const [empresa, setEmpresa] = useState('');
@@ -1131,10 +1130,10 @@ export default function MeetingRoom() {
{/* 3D Canvas - Sala Clara */} {/* 3D Canvas - Sala Clara */}
<div className="flex-1 h-full relative"> <div className="flex-1 h-full relative">
<Canvas camera={{ position: [0, 2.5, 2.8], fov: 45 }} shadows gl={{ localClippingEnabled: true }}> <Canvas camera={{ position: [0, 2.5, 2.8], fov: 45 }} shadows gl={{ localClippingEnabled: true }}>
<ThreeMeetingRoomScene currentActiveModel={activeModel} presentationState={presentationState} /> <ThreeMeetingRoomScene currentActiveModel={activeModel} presentationState={presentationState} isFocusMode={isFocusMode} />
{/* Renderizar avatares de TODOS na sala */} {/* Renderizar avatares de TODOS na sala - Ocultados se modo foco */}
{Object.keys(peers).map((chairKey) => { {!isFocusMode && Object.keys(peers).map((chairKey) => {
const peer = peers[chairKey]; const peer = peers[chairKey];
const chairConf = CHAIRS.find((c) => c.id === peer.chairId); const chairConf = CHAIRS.find((c) => c.id === peer.chairId);
if (!chairConf) return null; if (!chairConf) return null;
@@ -1160,7 +1159,7 @@ export default function MeetingRoom() {
minDistance={0.5} minDistance={0.5}
maxDistance={8} maxDistance={8}
maxPolarAngle={Math.PI / 2 - 0.05} maxPolarAngle={Math.PI / 2 - 0.05}
target={[0, 0.95, 0]} target={isFocusMode ? [0, 0, 0] : [0, 0.95, 0]}
/> />
</Canvas> </Canvas>
@@ -1334,7 +1333,7 @@ export default function MeetingRoom() {
</div> </div>
</div> </div>
{/* HUD Inferior - Controles de Áudio */} {/* HUD Inferior - Controles de Áudio & Modo Foco */}
<div className="absolute bottom-4 left-4 pointer-events-none z-10 flex gap-2"> <div className="absolute bottom-4 left-4 pointer-events-none z-10 flex gap-2">
<Button <Button
variant={isMuted ? 'destructive' : 'default'} variant={isMuted ? 'destructive' : 'default'}
@@ -1346,6 +1345,20 @@ export default function MeetingRoom() {
{isMuted ? <MicOff className="h-4.5 w-4.5" /> : <Mic className="h-4.5 w-4.5" />} {isMuted ? <MicOff className="h-4.5 w-4.5" /> : <Mic className="h-4.5 w-4.5" />}
</Button> </Button>
<Button
variant={isFocusMode ? 'secondary' : 'outline'}
size="icon"
className={`pointer-events-auto h-10 w-10 rounded-xl shadow-2xl border ${
isFocusMode
? 'bg-sky-500 hover:bg-sky-600 text-slate-950 border-sky-400'
: 'bg-slate-900/90 text-white border-slate-700 hover:bg-slate-800'
}`}
onClick={() => setIsFocusMode(!isFocusMode)}
title={isFocusMode ? 'Voltar para Sala Virtual (Minimizar)' : 'Focar na Peça (Tela Cheia)'}
>
{isFocusMode ? <Minimize2 className="h-4.5 w-4.5" /> : <Maximize2 className="h-4.5 w-4.5" />}
</Button>
<div className="pointer-events-auto rounded-xl border bg-slate-900/90 backdrop-blur-md px-4 py-2 flex flex-col justify-center shadow-2xl"> <div className="pointer-events-auto rounded-xl border bg-slate-900/90 backdrop-blur-md px-4 py-2 flex flex-col justify-center shadow-2xl">
<span className="font-mono text-[8px] text-muted-foreground uppercase"> <span className="font-mono text-[8px] text-muted-foreground uppercase">
{isPresenter ? 'Apresentação Ativa' : 'Observador'} {isPresenter ? 'Apresentação Ativa' : 'Observador'}