Changes
Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com>
This commit is contained in:
+146
-25
@@ -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<HTMLInputElement>(null);
|
||||
const rafRef = useRef<number | null>(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 (
|
||||
<button
|
||||
@@ -76,11 +102,17 @@ export function DevPanel({ simXR, onToggleSimXR }: DevPanelProps) {
|
||||
}
|
||||
|
||||
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="fixed top-20 right-2 z-50 w-80 rounded-lg border border-green-500/40 bg-black/90 font-mono text-[10px] text-green-300 shadow-2xl backdrop-blur">
|
||||
<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={exportReport}
|
||||
className="rounded border border-green-500/40 px-1.5 py-0.5 text-[9px] text-green-300 hover:bg-green-500/20"
|
||||
title="Baixa JSON + screenshot"
|
||||
>
|
||||
⬇ Relatório
|
||||
</button>
|
||||
<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'}`}
|
||||
@@ -91,26 +123,22 @@ export function DevPanel({ simXR, onToggleSimXR }: DevPanelProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="flex border-b border-green-500/20">
|
||||
{(['inputs', 'logs'] as Tab[]).map((t) => (
|
||||
{(['inputs', 'cena', 'xr', 'macro', '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'}`}
|
||||
className={`flex-1 px-1 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">
|
||||
<div className="max-h-[60vh] overflow-y-auto p-2">
|
||||
{tab === 'inputs' && (
|
||||
<div className="space-y-2">
|
||||
<div className="text-green-500/70">
|
||||
fake: {isFakeActive() ? 'ATIVO' : 'inativo'}
|
||||
</div>
|
||||
<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;
|
||||
@@ -133,6 +161,91 @@ export function DevPanel({ simXR, onToggleSimXR }: DevPanelProps) {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{tab === 'cena' && (
|
||||
<div className="space-y-1">
|
||||
<Row k="meshes" v={String(sceneStats.meshCount)} />
|
||||
<Row k="triângulos" v={sceneStats.triCount.toLocaleString()} />
|
||||
<Row k="bbox (m)" v={sceneStats.modelBboxSize.map(n => n.toFixed(3)).join(' × ')} />
|
||||
<Row k="pos (m)" v={sceneStats.modelGroupPos.map(n => n.toFixed(3)).join(', ')} />
|
||||
<Row k="rot (°)" v={sceneStats.modelGroupRotDeg.map(n => n.toFixed(1)).join(', ')} />
|
||||
<Row k="escala" v={sceneStats.modelGroupScale.toFixed(3)} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{tab === 'xr' && (
|
||||
<div className="space-y-1">
|
||||
<Row k="modo" v={sceneStats.xrMode ?? '—'} />
|
||||
<Row k="FPS" v={String(sceneStats.fps)} />
|
||||
<Row k="inputs" v={String(sceneStats.xrInputCount)} />
|
||||
<Row k="planos" v={String(sceneStats.xrPlanesCount)} />
|
||||
<Row k="hit-test" v={sceneStats.xrHitTestActive ? 'sim' : 'não'} />
|
||||
<Row k="navigator.xr" v={typeof navigator !== 'undefined' && 'xr' in navigator ? 'ok' : 'ausente'} />
|
||||
<Row k="HTTPS" v={location.protocol === 'https:' ? 'sim' : 'NÃO'} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{tab === 'macro' && (
|
||||
<div className="space-y-2">
|
||||
<div className="text-green-500/70">fase: <span className="text-green-300">{macroState.phase}</span></div>
|
||||
<div className="flex gap-1 flex-wrap">
|
||||
{macroState.phase !== 'recording' ? (
|
||||
<button onClick={startRecording} className="rounded border border-red-500/50 bg-red-500/10 px-2 py-0.5 text-red-300 hover:bg-red-500/20">● Gravar</button>
|
||||
) : (
|
||||
<button onClick={stopRecording} className="rounded border border-yellow-500/50 bg-yellow-500/10 px-2 py-0.5 text-yellow-300">■ Parar</button>
|
||||
)}
|
||||
{macroState.phase !== 'playing' ? (
|
||||
<button
|
||||
onClick={() => startPlaying()}
|
||||
disabled={!macroState.current}
|
||||
className="rounded border border-green-500/50 bg-green-500/10 px-2 py-0.5 text-green-300 hover:bg-green-500/20 disabled:opacity-40"
|
||||
>▶ Play</button>
|
||||
) : (
|
||||
<button onClick={stopPlaying} className="rounded border border-yellow-500/50 bg-yellow-500/10 px-2 py-0.5 text-yellow-300">⏹ Stop</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => macroState.current && downloadMacro(macroState.current)}
|
||||
disabled={!macroState.current}
|
||||
className="rounded border border-green-500/40 px-2 py-0.5 hover:bg-green-500/20 disabled:opacity-40"
|
||||
>⬇ Salvar</button>
|
||||
<button
|
||||
onClick={() => fileRef.current?.click()}
|
||||
className="rounded border border-green-500/40 px-2 py-0.5 hover:bg-green-500/20"
|
||||
>⬆ Carregar</button>
|
||||
<input
|
||||
ref={fileRef}
|
||||
type="file"
|
||||
accept="application/json"
|
||||
className="hidden"
|
||||
onChange={async (e) => {
|
||||
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 = '';
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{macroState.current && (
|
||||
<div className="rounded border border-green-500/20 p-1.5">
|
||||
<div className="font-bold text-green-300">{macroState.current.name}</div>
|
||||
<div>frames: {macroState.current.frames.length}</div>
|
||||
<div>duração: {(macroState.current.duration / 1000).toFixed(1)}s</div>
|
||||
</div>
|
||||
)}
|
||||
{macroState.phase === 'recording' && (
|
||||
<div className="text-yellow-300">gravando… {macroState.rec.length} frames</div>
|
||||
)}
|
||||
<div className="text-green-500/60 leading-tight border-t border-green-500/20 pt-1">
|
||||
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.
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{tab === 'logs' && (
|
||||
<div className="space-y-1">
|
||||
<input
|
||||
@@ -144,8 +257,7 @@ export function DevPanel({ simXR, onToggleSimXR }: DevPanelProps) {
|
||||
<div className="space-y-0.5">
|
||||
{logBuffer
|
||||
.filter((l) => !logFilter || l.msg.toLowerCase().includes(logFilter.toLowerCase()))
|
||||
.slice(-80)
|
||||
.reverse()
|
||||
.slice(-80).reverse()
|
||||
.map((l, i) => (
|
||||
<div
|
||||
key={i}
|
||||
@@ -164,3 +276,12 @@ export function DevPanel({ simXR, onToggleSimXR }: DevPanelProps) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Row({ k, v }: { k: string; v: string }) {
|
||||
return (
|
||||
<div className="flex justify-between gap-2 border-b border-green-500/10 py-0.5">
|
||||
<span className="text-green-500/70">{k}</span>
|
||||
<span className="text-green-200 break-all text-right">{v}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user