/** * DEVKIT — HTML overlay panel with diagnostics + macro recorder + export. * * Tabs: Inputs · Cena · XR · Macro · Logs * Export: snapshot JSON + canvas screenshot, both downloaded. */ import { useEffect, useRef, useState } from 'react'; import { fakeInput, isFakeActive } from './fakeInputStore'; import { sceneStats } from './sceneStatsStore'; import { macroState, subscribeMacro, startRecording, stopRecording, startPlaying, stopPlaying, downloadMacro, loadMacroFromFile, } from './macroStore'; 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' | 'cena' | 'xr' | 'macro' | '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 fileRef = useRef(null); const rafRef = useRef(null); useEffect(() => { installLogger(); }, []); useEffect(() => subscribeMacro(() => tick((n) => n + 1)), []); 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); }; }, []); const exportReport = async () => { const canvas = document.querySelector('canvas'); const screenshot = canvas ? canvas.toDataURL('image/png') : null; const report = { ts: new Date().toISOString(), url: window.location.href, userAgent: navigator.userAgent, simXR, fakeActive: isFakeActive(), stats: { ...sceneStats }, inputs: { left: { grip: fakeInput.left.gripValue, trigger: fakeInput.left.triggerValue }, right: { grip: fakeInput.right.gripValue, trigger: fakeInput.right.triggerValue }, }, logs: logBuffer.slice(-100), }; const blob = new Blob([JSON.stringify(report, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `devkit-report-${Date.now()}.json`; a.click(); URL.revokeObjectURL(url); if (screenshot) { const a2 = document.createElement('a'); a2.href = screenshot; a2.download = `devkit-screenshot-${Date.now()}.png`; a2.click(); } }; if (collapsed) { return ( ); } return (
🧪 DEVKIT
{(['inputs', 'cena', 'xr', 'macro', 'logs'] as Tab[]).map((t) => ( ))}
{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 === 'cena' && (
n.toFixed(3)).join(' × ')} /> n.toFixed(3)).join(', ')} /> n.toFixed(1)).join(', ')} />
)} {tab === 'xr' && (
)} {tab === 'macro' && (
fase: {macroState.phase}
{macroState.phase !== 'recording' ? ( ) : ( )} {macroState.phase !== 'playing' ? ( ) : ( )} { const f = e.target.files?.[0]; if (!f) return; try { const macro = await loadMacroFromFile(f); macroState.current = macro; tick((n) => n + 1); } catch { /* noop */ } e.target.value = ''; }} />
{macroState.current && (
{macroState.current.name}
frames: {macroState.current.frames.length}
duração: {(macroState.current.duration / 1000).toFixed(1)}s
)} {macroState.phase === 'recording' && (
gravando… {macroState.rec.length} frames
)}
Use os atalhos do teclado para encenar o bug; depois clique em Parar e Salvar. No próximo round, Carregar o JSON e clicar em Play reproduz exatamente.
)} {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…
}
)}
); } function Row({ k, v }: { k: string; v: string }) { return (
{k} {v}
); }