Files
SteelXR2/src/devkit/DevPanel.tsx
T
gpt-engineer-app[bot] 7772630f92 Changes
Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com>
2026-05-12 11:01:19 +00:00

167 lines
5.9 KiB
TypeScript

/**
* 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<Tab>('inputs');
const [collapsed, setCollapsed] = useState(false);
const [, tick] = useState(0);
const [logFilter, setLogFilter] = useState('');
const rafRef = useRef<number | null>(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 (
<button
onClick={() => setCollapsed(false)}
className="fixed top-20 right-2 z-50 rounded bg-black/85 px-2 py-1 font-mono text-[10px] text-green-400 border border-green-500/40 hover:bg-black"
>
🧪 DevKit
</button>
);
}
return (
<div className="fixed top-20 right-2 z-50 w-72 rounded-lg border border-green-500/40 bg-black/90 font-mono text-[10px] text-green-300 shadow-2xl backdrop-blur">
{/* Header */}
<div className="flex items-center justify-between border-b border-green-500/30 px-2 py-1.5">
<span className="font-bold text-green-400">🧪 DEVKIT</span>
<div className="flex items-center gap-1">
<button
onClick={onToggleSimXR}
className={`rounded px-1.5 py-0.5 text-[9px] ${simXR ? 'bg-green-500 text-black' : 'border border-green-500/40 text-green-300 hover:bg-green-500/20'}`}
>
{simXR ? 'SIM ON' : 'Simular XR'}
</button>
<button onClick={() => setCollapsed(true)} className="px-1 hover:text-white"></button>
</div>
</div>
{/* Tabs */}
<div className="flex border-b border-green-500/20">
{(['inputs', 'logs'] as Tab[]).map((t) => (
<button
key={t}
onClick={() => setTab(t)}
className={`flex-1 px-2 py-1 text-[10px] uppercase ${tab === t ? 'bg-green-500/20 text-green-200' : 'text-green-500/70 hover:text-green-300'}`}
>
{t}
</button>
))}
</div>
{/* Content */}
<div className="max-h-[55vh] overflow-y-auto p-2">
{tab === 'inputs' && (
<div className="space-y-2">
<div className="text-green-500/70">
fake: {isFakeActive() ? 'ATIVO' : 'inativo'}
</div>
{(['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 (
<div key={h} className="rounded border border-green-500/20 p-1.5">
<div className={`font-bold ${grabbing ? 'text-yellow-300' : 'text-green-300'}`}>
{h.toUpperCase()} {grabbing && '✊ GRAB'}
</div>
<div>grip: {s.gripValue.toFixed(2)} trg: {s.triggerValue.toFixed(2)}</div>
<div>pose: {p.map((v) => v.toFixed(2)).join(', ')}</div>
</div>
);
})}
<div className="border-t border-green-500/20 pt-2 text-green-500/70">
<div className="font-bold text-green-400 mb-1">Atalhos teclado</div>
<div>Q/W = grip L/R · A/S = trigger L/R</div>
<div>1/2 = ativar mão · setas = mover</div>
<div>Shift+/ = Z (frente/trás) · R = reset</div>
</div>
</div>
)}
{tab === 'logs' && (
<div className="space-y-1">
<input
value={logFilter}
onChange={(e) => 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"
/>
<div className="space-y-0.5">
{logBuffer
.filter((l) => !logFilter || l.msg.toLowerCase().includes(logFilter.toLowerCase()))
.slice(-80)
.reverse()
.map((l, i) => (
<div
key={i}
className={`break-all leading-tight ${
l.level === 'error' ? 'text-red-400' : l.level === 'warn' ? 'text-yellow-300' : 'text-green-300/90'
}`}
>
{l.msg}
</div>
))}
{logBuffer.length === 0 && <div className="text-green-500/50 italic">sem logs ainda</div>}
</div>
</div>
)}
</div>
</div>
);
}