diff --git a/src/devkit/DevPanel.tsx b/src/devkit/DevPanel.tsx index 63115c3..9303d96 100644 --- a/src/devkit/DevPanel.tsx +++ b/src/devkit/DevPanel.tsx @@ -1,24 +1,21 @@ /** - * DEVKIT — HTML overlay panel with diagnostics. + * DEVKIT — HTML overlay panel with diagnostics + macro recorder + export. * - * 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. + * 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; -} +interface LogEntry { ts: number; level: 'log' | 'warn' | 'error'; msg: string; } const logBuffer: LogEntry[] = []; let installed = false; - function installLogger() { if (installed) return; installed = true; @@ -38,7 +35,7 @@ function installLogger() { }); } -type Tab = 'inputs' | 'logs'; +type Tab = 'inputs' | 'cena' | 'xr' | 'macro' | 'logs'; interface DevPanelProps { simXR: boolean; @@ -50,9 +47,11 @@ export function DevPanel({ simXR, onToggleSimXR }: DevPanelProps) { 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; @@ -64,6 +63,33 @@ export function DevPanel({ simXR, onToggleSimXR }: DevPanelProps) { 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 ( ))} - {/* Content */} -
+
{tab === 'inputs' && (
-
- fake: {isFakeActive() ? 'ATIVO' : 'inativo'} -
+
fake: {isFakeActive() ? 'ATIVO' : 'inativo'}
{(['left', 'right'] as const).map((h) => { const s = fakeInput[h]; const grabbing = s.gripValue > 0.7; @@ -133,6 +161,91 @@ export function DevPanel({ simXR, onToggleSimXR }: DevPanelProps) {
)} + + {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' && (
{logBuffer .filter((l) => !logFilter || l.msg.toLowerCase().includes(logFilter.toLowerCase())) - .slice(-80) - .reverse() + .slice(-80).reverse() .map((l, i) => (
); } + +function Row({ k, v }: { k: string; v: string }) { + return ( +
+ {k} + {v} +
+ ); +}