/** * DEVKIT — HTML overlay panel with diagnostics. * * Tabs: * - Inputs: live grip/trigger/pose values for both controllers (real or fake) * - Logs: captures last 100 console.log/warn/error entries with filter * * Always visible when ?devkit=1, top-right corner. */ import { useEffect, useRef, useState } from 'react'; import { fakeInput, isFakeActive } from './fakeInputStore'; interface LogEntry { ts: number; level: 'log' | 'warn' | 'error'; msg: string; } const logBuffer: LogEntry[] = []; let installed = false; function installLogger() { if (installed) return; installed = true; (['log', 'warn', 'error'] as const).forEach((level) => { const orig = console[level].bind(console); console[level] = (...args: unknown[]) => { try { const msg = args.map((a) => { if (typeof a === 'string') return a; try { return JSON.stringify(a); } catch { return String(a); } }).join(' '); logBuffer.push({ ts: Date.now(), level, msg }); if (logBuffer.length > 200) logBuffer.shift(); } catch { /* noop */ } orig(...args); }; }); } type Tab = 'inputs' | 'logs'; interface DevPanelProps { simXR: boolean; onToggleSimXR: () => void; } export function DevPanel({ simXR, onToggleSimXR }: DevPanelProps) { const [tab, setTab] = useState('inputs'); const [collapsed, setCollapsed] = useState(false); const [, tick] = useState(0); const [logFilter, setLogFilter] = useState(''); const rafRef = useRef(null); useEffect(() => { installLogger(); }, []); useEffect(() => { let last = 0; const loop = (t: number) => { if (t - last > 150) { tick((n) => n + 1); last = t; } rafRef.current = requestAnimationFrame(loop); }; rafRef.current = requestAnimationFrame(loop); return () => { if (rafRef.current) cancelAnimationFrame(rafRef.current); }; }, []); if (collapsed) { return ( ); } return (
{/* Header */}
🧪 DEVKIT
{/* Tabs */}
{(['inputs', 'logs'] as Tab[]).map((t) => ( ))}
{/* Content */}
{tab === 'inputs' && (
fake: {isFakeActive() ? 'ATIVO' : 'inativo'}
{(['left', 'right'] as const).map((h) => { const s = fakeInput[h]; const grabbing = s.gripValue > 0.7; const p = [s.gripWorld.elements[12], s.gripWorld.elements[13], s.gripWorld.elements[14]]; return (
{h.toUpperCase()} {grabbing && '✊ GRAB'}
grip: {s.gripValue.toFixed(2)} trg: {s.triggerValue.toFixed(2)}
pose: {p.map((v) => v.toFixed(2)).join(', ')}
); })}
Atalhos teclado
Q/W = grip L/R · A/S = trigger L/R
1/2 = ativar mão · setas = mover
Shift+↑/↓ = Z (frente/trás) · R = reset
)} {tab === 'logs' && (
setLogFilter(e.target.value)} placeholder="filtro… (ex: [XR])" className="w-full rounded border border-green-500/30 bg-black/60 px-1 py-0.5 text-green-300" />
{logBuffer .filter((l) => !logFilter || l.msg.toLowerCase().includes(logFilter.toLowerCase())) .slice(-80) .reverse() .map((l, i) => (
{l.msg}
))} {logBuffer.length === 0 &&
sem logs ainda…
}
)}
); }