Changes
Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,166 @@
|
|||||||
|
/**
|
||||||
|
* 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
/**
|
||||||
|
* DEVKIT — Visual fake controllers + keyboard driver.
|
||||||
|
*
|
||||||
|
* Renders two spheres (L=blue, R=red) at the fake controller positions and
|
||||||
|
* listens to global keyboard for grip/trigger/movement. Mounted only when
|
||||||
|
* SimXR is active.
|
||||||
|
*
|
||||||
|
* Keys:
|
||||||
|
* Q / W → hold grip Left / Right (analog = 1.0)
|
||||||
|
* A / S → hold trigger Left / Right (analog = 1.0)
|
||||||
|
* 1 / 2 → select Left / Right as the active hand
|
||||||
|
* Arrows → move active hand on X (←→) / Y (↑↓)
|
||||||
|
* Shift+Arrows→ move active hand on Z (↑=forward, ↓=back)
|
||||||
|
* R → reset hand poses
|
||||||
|
*/
|
||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
import { useFrame } from '@react-three/fiber';
|
||||||
|
import * as THREE from 'three';
|
||||||
|
import { fakeInput, setFakeActive } from './fakeInputStore';
|
||||||
|
|
||||||
|
const STEP = 0.03; // m per keypress
|
||||||
|
|
||||||
|
export function FakeControllers() {
|
||||||
|
const leftMesh = useRef<THREE.Mesh>(null);
|
||||||
|
const rightMesh = useRef<THREE.Mesh>(null);
|
||||||
|
const [, force] = useState(0);
|
||||||
|
const activeHand = useRef<'left' | 'right'>('right');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setFakeActive(true);
|
||||||
|
return () => setFakeActive(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const keys = new Set<string>();
|
||||||
|
|
||||||
|
const apply = () => {
|
||||||
|
const pos = new THREE.Vector3();
|
||||||
|
const quat = new THREE.Quaternion();
|
||||||
|
const scl = new THREE.Vector3();
|
||||||
|
const hand = fakeInput[activeHand.current];
|
||||||
|
hand.gripWorld.decompose(pos, quat, scl);
|
||||||
|
|
||||||
|
const dx = (keys.has('ArrowRight') ? STEP : 0) - (keys.has('ArrowLeft') ? STEP : 0);
|
||||||
|
const dyOrZ = (keys.has('ArrowUp') ? STEP : 0) - (keys.has('ArrowDown') ? STEP : 0);
|
||||||
|
const shift = keys.has('Shift');
|
||||||
|
pos.x += dx;
|
||||||
|
if (shift) pos.z -= dyOrZ; // up = forward (-Z)
|
||||||
|
else pos.y += dyOrZ;
|
||||||
|
|
||||||
|
hand.gripWorld.compose(pos, quat, scl);
|
||||||
|
|
||||||
|
// grip / trigger
|
||||||
|
fakeInput.left.gripValue = keys.has('q') ? 1 : 0;
|
||||||
|
fakeInput.right.gripValue = keys.has('w') ? 1 : 0;
|
||||||
|
fakeInput.left.triggerValue = keys.has('a') ? 1 : 0;
|
||||||
|
fakeInput.right.triggerValue = keys.has('s') ? 1 : 0;
|
||||||
|
|
||||||
|
force((n) => n + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDown = (e: KeyboardEvent) => {
|
||||||
|
const k = e.key === 'Shift' ? 'Shift' : e.key;
|
||||||
|
keys.add(k);
|
||||||
|
if (k === '1') activeHand.current = 'left';
|
||||||
|
else if (k === '2') activeHand.current = 'right';
|
||||||
|
else if (k === 'r' || k === 'R') {
|
||||||
|
fakeInput.left.gripWorld.makeTranslation(-0.25, 1.2, -0.5);
|
||||||
|
fakeInput.right.gripWorld.makeTranslation(0.25, 1.2, -0.5);
|
||||||
|
force((n) => n + 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
apply();
|
||||||
|
};
|
||||||
|
const onUp = (e: KeyboardEvent) => {
|
||||||
|
const k = e.key === 'Shift' ? 'Shift' : e.key;
|
||||||
|
keys.delete(k);
|
||||||
|
apply();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('keydown', onDown);
|
||||||
|
window.addEventListener('keyup', onUp);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('keydown', onDown);
|
||||||
|
window.removeEventListener('keyup', onUp);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Sync mesh visuals to fake state each frame
|
||||||
|
useFrame(() => {
|
||||||
|
if (leftMesh.current) {
|
||||||
|
const p = new THREE.Vector3().setFromMatrixPosition(fakeInput.left.gripWorld);
|
||||||
|
leftMesh.current.position.copy(p);
|
||||||
|
const grabbing = fakeInput.left.gripValue > 0.7;
|
||||||
|
const mat = leftMesh.current.material as THREE.MeshStandardMaterial;
|
||||||
|
mat.emissiveIntensity = grabbing ? 1.5 : 0.4;
|
||||||
|
}
|
||||||
|
if (rightMesh.current) {
|
||||||
|
const p = new THREE.Vector3().setFromMatrixPosition(fakeInput.right.gripWorld);
|
||||||
|
rightMesh.current.position.copy(p);
|
||||||
|
const grabbing = fakeInput.right.gripValue > 0.7;
|
||||||
|
const mat = rightMesh.current.material as THREE.MeshStandardMaterial;
|
||||||
|
mat.emissiveIntensity = grabbing ? 1.5 : 0.4;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<mesh ref={leftMesh}>
|
||||||
|
<sphereGeometry args={[0.04, 16, 16]} />
|
||||||
|
<meshStandardMaterial color="#3b82f6" emissive="#3b82f6" emissiveIntensity={0.4} />
|
||||||
|
</mesh>
|
||||||
|
<mesh ref={rightMesh}>
|
||||||
|
<sphereGeometry args={[0.04, 16, 16]} />
|
||||||
|
<meshStandardMaterial color="#ef4444" emissive="#ef4444" emissiveIntensity={0.4} />
|
||||||
|
</mesh>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user