- {(['inputs', 'logs'] as Tab[]).map((t) => (
+ {(['inputs', 'cena', 'xr', 'macro', 'logs'] as Tab[]).map((t) => (
))}
+
{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 +164,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}
+
+ );
+}
diff --git a/src/devkit/FakeControllers.tsx b/src/devkit/FakeControllers.tsx
index 58e421f..eb22ae2 100644
--- a/src/devkit/FakeControllers.tsx
+++ b/src/devkit/FakeControllers.tsx
@@ -17,6 +17,7 @@ import { useEffect, useRef, useState } from 'react';
import { useFrame } from '@react-three/fiber';
import * as THREE from 'three';
import { fakeInput, setFakeActive } from './fakeInputStore';
+import { isPlaybackActive } from './macroStore';
const STEP = 0.03; // m per keypress
@@ -35,6 +36,7 @@ export function FakeControllers() {
const keys = new Set
();
const apply = () => {
+ if (isPlaybackActive()) return; // macro playback owns inputs
const pos = new THREE.Vector3();
const quat = new THREE.Quaternion();
const scl = new THREE.Vector3();
diff --git a/src/devkit/MacroDriver.tsx b/src/devkit/MacroDriver.tsx
new file mode 100644
index 0000000..88505a4
--- /dev/null
+++ b/src/devkit/MacroDriver.tsx
@@ -0,0 +1,11 @@
+/**
+ * DEVKIT — Drives macro recording/playback inside the R3F frame loop.
+ * Mounted inside