🚀 Auto-deploy: melhoria no snap e medição AR em 25/05/2026 00:21:23

This commit is contained in:
2026-05-25 00:21:23 +00:00
parent e390fd0f59
commit 3c903c1ff1
2 changed files with 93 additions and 45 deletions
+2 -1
View File
@@ -25,7 +25,7 @@ const Index = () => {
const { model, addModel, models, maxModels, xrSupported, setXrSupported } = useModelStore(); const { model, addModel, models, maxModels, xrSupported, setXrSupported } = useModelStore();
// --- Painel de Logs Remotos em Tempo Real --- // --- 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'); const [showLogs, setShowLogs] = useState(() => new URLSearchParams(window.location.search).get('viewlogs') === 'true');
useEffect(() => { useEffect(() => {
@@ -204,6 +204,7 @@ const Index = () => {
type="file" type="file"
accept={ACCEPTED_EXTENSIONS} accept={ACCEPTED_EXTENSIONS}
className="hidden" className="hidden"
title="Importar arquivo de modelo 3D"
onChange={handleFileUpload} /> onChange={handleFileUpload} />
+91 -44
View File
@@ -1,24 +1,48 @@
import { useEffect, useRef, useState, useMemo, Suspense } from 'react'; import { useEffect, useRef, useState, useMemo, Suspense } from 'react';
import { useParams, useNavigate } from 'react-router-dom'; 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 { OrbitControls, Text, useGLTF } from '@react-three/drei';
import * as THREE from 'three'; import * as THREE from 'three';
import { supabase } from '@/integrations/supabase/client'; import { supabase } from '@/integrations/supabase/client';
import { useModelStore } from '@/stores/useModelStore'; import { useModelStore } from '@/stores/useModelStore';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Checkbox } from '@/components/ui/checkbox'; import { Checkbox } from '@/components/ui/checkbox';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { import {
Users, MessageSquare, Mic, MicOff, Home, Send, Slider, Users, MessageSquare, Mic, MicOff, Home, Send,
ArrowRight, ChevronLeft, ChevronRight, Share2, LogOut, Check, ArrowRight, ChevronLeft, ChevronRight, Share2, LogOut,
Scissors, RotateCw, Maximize2, ShieldAlert, Award RotateCw, Award
} from 'lucide-react'; } 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 --- // --- 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 = [ const CHAIRS = [
{ id: '1', name: 'Assento 1', pos: [-1.2, 0.95, -1.45], lookAt: [0, 0.95, 0] }, { 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] }, { 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] }, { 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: MiiPreset[] = [
const AVATAR_PRESETS = [
{ id: 1, name: 'Mii Cyber', desc: 'Cabelo curto preto e óculos vermelhos', color: '#ef4444' }, { 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: 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: 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' }, { 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' }, { 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']; 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 --- // --- Subcomponentes 3D ---
// Avatar estilo Mii (Nintendo Wii) // 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<string, unknown>) {
const groupRef = useRef<THREE.Group>(null); const groupRef = useRef<THREE.Group>(null);
// Parâmetros do rosto e cabelo com base no ID
const mii = useMemo(() => { const mii = useMemo(() => {
const skin = SKIN_COLORS[(avatarId - 1) % SKIN_COLORS.length]; const skin = SKIN_COLORS[(avatarId - 1) % SKIN_COLORS.length];
return { return {
@@ -71,20 +109,34 @@ function MiiAvatar({ avatarId, username, isTalking, ...props }: { avatarId: numb
}, [avatarId]); }, [avatarId]);
useFrame((state) => { useFrame((state) => {
if (groupRef.current) { if (groupRef.current && props.position && Array.isArray(props.position) && props.position.length >= 2) {
groupRef.current.position.y = props.position[1] + Math.sin(state.clock.getElapsedTime() * 2 + avatarId) * 0.012; 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 ( return (
<group ref={groupRef} {...props}> <group ref={groupRef} position={positionVector} rotation={rotationEuler}>
{/* Balão de Nome acima da cabeça */} {/* Balão de Nome acima da cabeça */}
<group position={[0, 0.28, 0]}> <group position={[0, 0.28, 0]}>
<mesh position={[0, 0, -0.001]}> <mesh position={[0, 0, -0.001]}>
<planeGeometry args={[0.22, 0.05]} /> <planeGeometry args={[0.22, 0.05]} />
<meshBasicMaterial color="#0f172a" transparent opacity={0.7} /> <meshBasicMaterial color="#0f172a" transparent opacity={0.7} />
</mesh> </mesh>
<Text fontSize={0.024} color="#ffffff" anchorX="center" anchorY="middle" fontStyle="bold"> <Text fontSize={0.024} color="#ffffff" anchorX="center" anchorY="middle" fontWeight="bold">
{username} {username}
</Text> </Text>
</group> </group>
@@ -138,7 +190,7 @@ function MiiAvatar({ avatarId, username, isTalking, ...props }: { avatarId: numb
<sphereGeometry args={[0.08, 16, 16]} /> <sphereGeometry args={[0.08, 16, 16]} />
<meshStandardMaterial color={mii.capColor} roughness={0.5} /> <meshStandardMaterial color={mii.capColor} roughness={0.5} />
</mesh> </mesh>
{/* Aba do Boné (virada para trás) */} {/* Aba do Boné */}
<mesh position={[0, 0, -0.08]} rotation={[0.1, 0, 0]}> <mesh position={[0, 0, -0.08]} rotation={[0.1, 0, 0]}>
<boxGeometry args={[0.11, 0.008, 0.08]} /> <boxGeometry args={[0.11, 0.008, 0.08]} />
<meshStandardMaterial color={mii.capColor} roughness={0.5} /> <meshStandardMaterial color={mii.capColor} roughness={0.5} />
@@ -197,12 +249,10 @@ function MiiAvatar({ avatarId, username, isTalking, ...props }: { avatarId: numb
{/* BARBA E BIGODE */} {/* BARBA E BIGODE */}
{mii.beard && ( {mii.beard && (
<group> <group>
{/* Bigode */}
<mesh position={[0, 0.075, 0.075]}> <mesh position={[0, 0.075, 0.075]}>
<boxGeometry args={[0.04, 0.006, 0.005]} /> <boxGeometry args={[0.04, 0.006, 0.005]} />
<meshBasicMaterial color="#713f12" /> <meshBasicMaterial color="#713f12" />
</mesh> </mesh>
{/* Barbicha */}
<mesh position={[0, 0.045, 0.072]} scale={[1.2, 1, 1]}> <mesh position={[0, 0.045, 0.072]} scale={[1.2, 1, 1]}>
<sphereGeometry args={[0.015, 8, 8]} /> <sphereGeometry args={[0.015, 8, 8]} />
<meshStandardMaterial color="#713f12" roughness={0.8} /> <meshStandardMaterial color="#713f12" roughness={0.8} />
@@ -213,17 +263,14 @@ function MiiAvatar({ avatarId, username, isTalking, ...props }: { avatarId: numb
{/* ÓCULOS */} {/* ÓCULOS */}
{mii.hasGlasses && ( {mii.hasGlasses && (
<group position={[0, 0.11, 0.072]}> <group position={[0, 0.11, 0.072]}>
{/* Lente esquerda */}
<mesh position={[-0.028, 0, 0.002]}> <mesh position={[-0.028, 0, 0.002]}>
<ringGeometry args={[0.016, 0.019, 16]} /> <ringGeometry args={[0.016, 0.019, 16]} />
<meshBasicMaterial color={mii.glassesColor} /> <meshBasicMaterial color={mii.glassesColor} />
</mesh> </mesh>
{/* Lente direita */}
<mesh position={[0.028, 0, 0.002]}> <mesh position={[0.028, 0, 0.002]}>
<ringGeometry args={[0.016, 0.019, 16]} /> <ringGeometry args={[0.016, 0.019, 16]} />
<meshBasicMaterial color={mii.glassesColor} /> <meshBasicMaterial color={mii.glassesColor} />
</mesh> </mesh>
{/* Ponte */}
<mesh position={[0, 0, 0.002]}> <mesh position={[0, 0, 0.002]}>
<boxGeometry args={[0.028, 0.004, 0.002]} /> <boxGeometry args={[0.028, 0.004, 0.002]} />
<meshBasicMaterial color={mii.glassesColor} /> <meshBasicMaterial color={mii.glassesColor} />
@@ -235,8 +282,7 @@ function MiiAvatar({ avatarId, username, isTalking, ...props }: { avatarId: numb
} }
// Carregador e Escalador Inteligente da Maquete Holográfica // 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: PresentationState }) {
function HologramModel({ model, presentationState }: { model: { url: string }; presentationState: any }) {
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);
@@ -270,7 +316,6 @@ function HologramModel({ model, presentationState }: { model: { url: string }; p
groupRef.current.rotation.z = rotation[2]; groupRef.current.rotation.z = rotation[2];
} }
if (scale) { if (scale) {
// Multiplica a escala base inteligente pelo fator da apresentação
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);
@@ -303,13 +348,11 @@ function HologramModel({ model, presentationState }: { model: { url: string }; p
return; return;
} }
// Vetor normal baseado no eixo e na inversão do corte
const normal = new THREE.Vector3(); const normal = new THREE.Vector3();
if (sectionAxis === 'x') normal.set(sectionInvert ? -1 : 1, 0, 0); if (sectionAxis === 'x') normal.set(sectionInvert ? -1 : 1, 0, 0);
else if (sectionAxis === 'y') normal.set(0, sectionInvert ? -1 : 1, 0); else if (sectionAxis === 'y') normal.set(0, sectionInvert ? -1 : 1, 0);
else if (sectionAxis === 'z') normal.set(0, 0, sectionInvert ? -1 : 1); else if (sectionAxis === 'z') normal.set(0, 0, sectionInvert ? -1 : 1);
// O plano corta na coordenada desejada
const plane = new THREE.Plane(normal, sectionLevel); const plane = new THREE.Plane(normal, sectionLevel);
clone.traverse((obj) => { clone.traverse((obj) => {
@@ -335,7 +378,7 @@ 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: any; presentationState: any }) { function ThreeMeetingRoomScene({ currentActiveModel, presentationState }: { currentActiveModel: { url: string; fileName: string } | null; presentationState: PresentationState }) {
const tableGeom = useMemo(() => { const tableGeom = useMemo(() => {
// Geometria da mesa em U // Geometria da mesa em U
const shape = new THREE.Shape(); const shape = new THREE.Shape();
@@ -471,7 +514,7 @@ function ThreeMeetingRoomScene({ currentActiveModel, presentationState }: { curr
))} ))}
{/* Iluminação Clara da Sala */} {/* Iluminação Clara da Sala */}
<hemisphereLight skyColor="#ffffff" groundColor="#64748b" intensity={1.4} /> <hemisphereLight color="#ffffff" groundColor="#64748b" intensity={1.4} />
<directionalLight position={[6, 12, 6]} intensity={1.8} castShadow /> <directionalLight position={[6, 12, 6]} intensity={1.8} castShadow />
<directionalLight position={[-6, 8, -6]} intensity={0.5} /> <directionalLight position={[-6, 8, -6]} intensity={0.5} />
<pointLight position={[0, 3.5, 0]} intensity={2.0} color="#0ea5e9" distance={8} /> <pointLight position={[0, 3.5, 0]} intensity={2.0} color="#0ea5e9" distance={8} />
@@ -497,7 +540,7 @@ export default function MeetingRoom() {
const [inputRoomId, setInputRoomId] = useState(roomId || ''); const [inputRoomId, setInputRoomId] = useState(roomId || '');
// Estados da Sala // Estados da Sala
const [peers, setPeers] = useState<Record<string, { username: string; avatarId: number; chairId: string; role: string; cargo: string; empresa: string; presence_ref: string }>>({}); const [peers, setPeers] = useState<Record<string, PeerData>>({});
const [chatMessages, setChatMessages] = useState<{ id: string; sender: string; text: string; time: string }[]>([]); const [chatMessages, setChatMessages] = useState<{ id: string; sender: string; text: string; time: string }[]>([]);
const [newMessage, setNewMessage] = useState(''); const [newMessage, setNewMessage] = useState('');
const [isMuted, setIsMuted] = useState(false); const [isMuted, setIsMuted] = useState(false);
@@ -507,7 +550,7 @@ export default function MeetingRoom() {
const [activeTab, setActiveTab] = useState<'chat' | 'participants'>('participants'); const [activeTab, setActiveTab] = useState<'chat' | 'participants'>('participants');
// Estado compartilhado da Apresentação da maquete // Estado compartilhado da Apresentação da maquete
const [presentationState, setPresentationState] = useState({ const [presentationState, setPresentationState] = useState<PresentationState>({
rotation: [0, 0, 0], rotation: [0, 0, 0],
scale: 1, scale: 1,
sectionEnabled: false, sectionEnabled: false,
@@ -516,7 +559,7 @@ export default function MeetingRoom() {
sectionInvert: false sectionInvert: false
}); });
const channelRef = useRef<any>(null); const channelRef = useRef<ReturnType<typeof supabase.channel> | null>(null);
const chatScrollRef = useRef<HTMLDivElement>(null); const chatScrollRef = useRef<HTMLDivElement>(null);
// Se o roomId mudar na url, atualiza o checkbox isPresenter // Se o roomId mudar na url, atualiza o checkbox isPresenter
@@ -544,10 +587,10 @@ export default function MeetingRoom() {
}, [chatMessages]); }, [chatMessages]);
// Envia as atualizações de apresentação via Broadcast para a sala inteira // Envia as atualizações de apresentação via Broadcast para a sala inteira
const handlePresentationChange = (updatedFields: Partial<typeof presentationState>) => { const handlePresentationChange = (updatedFields: Partial<PresentationState>) => {
if (!isPresenter || !channelRef.current) return; if (!isPresenter || !channelRef.current) return;
const newState = { ...presentationState, ...updatedFields }; const newState = { ...presentationState, ...updatedFields } as PresentationState;
setPresentationState(newState); setPresentationState(newState);
channelRef.current.send({ channelRef.current.send({
@@ -581,11 +624,11 @@ export default function MeetingRoom() {
channel channel
.on('presence', { event: 'sync' }, () => { .on('presence', { event: 'sync' }, () => {
const state = channel.presenceState(); const state = channel.presenceState();
const formattedPeers: Record<string, any> = {}; const formattedPeers: Record<string, PeerData> = {};
let count = 0; let count = 0;
Object.keys(state).forEach((chairKey) => { Object.keys(state).forEach((chairKey) => {
const presences = state[chairKey] as any[]; const presences = state[chairKey] as unknown as PeerData[];
if (presences && presences.length > 0) { if (presences && presences.length > 0) {
formattedPeers[chairKey] = presences[0]; formattedPeers[chairKey] = presences[0];
count++; count++;
@@ -595,10 +638,10 @@ export default function MeetingRoom() {
setActiveUsersCount(count); setActiveUsersCount(count);
}) })
.on('presence', { event: 'join' }, ({ key, newPresences }) => { .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 }) => { .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 // 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) // 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); setPresentationState(payload.payload as PresentationState);
} }
}); });
@@ -886,7 +929,7 @@ export default function MeetingRoom() {
<span className="font-mono text-xs font-bold text-primary flex items-center gap-1.5"> <span className="font-mono text-xs font-bold text-primary flex items-center gap-1.5">
<Award className="h-4 w-4 animate-pulse" /> Painel do Apresentador <Award className="h-4 w-4 animate-pulse" /> Painel do Apresentador
</span> </span>
<span className="font-mono text-[8px] bg-emerald-500/25 text-emerald-400 px-1.5 py-0.5 rounded uppercase">Sincronizado</span> <span className="font-mono text-[8px] bg-emerald-500/25 text-emerald-400 px-1.5 py-0.5 rounded uppercase font-semibold">Sincronizado</span>
</div> </div>
{/* Rotação Y */} {/* Rotação Y */}
@@ -900,6 +943,7 @@ export default function MeetingRoom() {
min={-Math.PI} min={-Math.PI}
max={Math.PI} max={Math.PI}
step={0.05} step={0.05}
title="Giro Horizontal"
value={presentationState.rotation[1]} value={presentationState.rotation[1]}
onChange={(e) => handlePresentationChange({ rotation: [0, parseFloat(e.target.value), 0] })} 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" 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} min={0.3}
max={3.0} max={3.0}
step={0.1} step={0.1}
title="Zoom da Peça"
value={presentationState.scale} value={presentationState.scale}
onChange={(e) => handlePresentationChange({ scale: parseFloat(e.target.value) })} onChange={(e) => handlePresentationChange({ scale: parseFloat(e.target.value) })}
className="w-full h-1 bg-slate-800 rounded-lg appearance-none cursor-pointer accent-primary" className="w-full h-1 bg-slate-800 rounded-lg appearance-none cursor-pointer accent-primary"
@@ -930,6 +975,7 @@ export default function MeetingRoom() {
<input <input
type="checkbox" type="checkbox"
id="section-cuts-enable" id="section-cuts-enable"
title="Habilitar Plano de Corte"
checked={presentationState.sectionEnabled} checked={presentationState.sectionEnabled}
onChange={(e) => handlePresentationChange({ sectionEnabled: e.target.checked })} onChange={(e) => handlePresentationChange({ sectionEnabled: e.target.checked })}
className="rounded border-slate-800 bg-slate-950 accent-primary cursor-pointer h-3.5 w-3.5" 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} min={-0.5}
max={0.5} max={0.5}
step={0.01} step={0.01}
title="Nível de Corte"
value={presentationState.sectionLevel} value={presentationState.sectionLevel}
onChange={(e) => handlePresentationChange({ sectionLevel: parseFloat(e.target.value) })} onChange={(e) => handlePresentationChange({ sectionLevel: parseFloat(e.target.value) })}
className="w-full h-1 bg-slate-800 rounded-lg appearance-none cursor-pointer accent-primary" className="w-full h-1 bg-slate-800 rounded-lg appearance-none cursor-pointer accent-primary"
@@ -978,6 +1025,7 @@ export default function MeetingRoom() {
<input <input
type="checkbox" type="checkbox"
id="invert-section" id="invert-section"
title="Inverter Plano de Corte"
checked={presentationState.sectionInvert} checked={presentationState.sectionInvert}
onChange={(e) => handlePresentationChange({ sectionInvert: e.target.checked })} onChange={(e) => handlePresentationChange({ sectionInvert: e.target.checked })}
className="rounded border-slate-800 bg-slate-950 accent-primary cursor-pointer h-3 w-3" 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) => { {Object.keys(peers).map((chairKey) => {
const peer = peers[chairKey]; const peer = peers[chairKey];
const isHost = peer.role === 'presenter'; const isHost = peer.role === 'presenter';
const avatar = AVATAR_PRESETS[peer.avatarId - 1] || AVATAR_PRESETS[0];
const isMe = peer.chairId === selectedChair; const isMe = peer.chairId === selectedChair;
return ( return (
<div key={peer.presence_ref} className="rounded-lg border border-slate-800 bg-slate-950/40 p-3 flex items-start gap-3 relative overflow-hidden"> <div key={peer.presence_ref} className="rounded-lg border border-slate-800 bg-slate-950/40 p-3 flex items-start gap-3 relative overflow-hidden">
{/* Indicador da cor do avatar do Mii */} {/* Indicador da cor do avatar do Mii via classe estática Tailwind */}
<div className="h-8 w-8 rounded-full border border-slate-800 flex items-center justify-center font-bold text-xs" style={{ backgroundColor: `${avatar.color}35`, color: avatar.color }}> <div className={`h-8 w-8 rounded-full border border-slate-800 flex items-center justify-center font-bold text-xs ${getAvatarBgClass(peer.avatarId)}`}>
{peer.username.substring(0, 2).toUpperCase()} {peer.username.substring(0, 2).toUpperCase()}
</div> </div>