Changes
Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* DEVKIT — Macro recorder/player for fake controller inputs.
|
||||
*
|
||||
* Records fakeInput poses + grip/trigger at ~30Hz to a JSON-serializable
|
||||
* timeline. Replays by writing values back into fakeInput. Used to reproduce
|
||||
* bugs reported by the headset tester.
|
||||
*/
|
||||
import * as THREE from 'three';
|
||||
import { fakeInput } from './fakeInputStore';
|
||||
|
||||
export interface MacroFrame {
|
||||
t: number; // ms since start
|
||||
l: { m: number[]; g: number; t: number };
|
||||
r: { m: number[]; g: number; t: number };
|
||||
}
|
||||
|
||||
export interface Macro {
|
||||
name: string;
|
||||
duration: number;
|
||||
frames: MacroFrame[];
|
||||
}
|
||||
|
||||
type Phase = 'idle' | 'recording' | 'playing';
|
||||
|
||||
interface State {
|
||||
phase: Phase;
|
||||
recStart: number;
|
||||
rec: MacroFrame[];
|
||||
play: { macro: Macro; start: number; idx: number } | null;
|
||||
current: Macro | null;
|
||||
listeners: Set<() => void>;
|
||||
}
|
||||
|
||||
export const macroState: State = {
|
||||
phase: 'idle',
|
||||
recStart: 0,
|
||||
rec: [],
|
||||
play: null,
|
||||
current: null,
|
||||
listeners: new Set(),
|
||||
};
|
||||
|
||||
function notify() { for (const l of macroState.listeners) l(); }
|
||||
|
||||
export function subscribeMacro(cb: () => void) {
|
||||
macroState.listeners.add(cb);
|
||||
return () => macroState.listeners.delete(cb);
|
||||
}
|
||||
|
||||
export function startRecording() {
|
||||
macroState.phase = 'recording';
|
||||
macroState.recStart = performance.now();
|
||||
macroState.rec = [];
|
||||
notify();
|
||||
}
|
||||
|
||||
export function stopRecording(): Macro {
|
||||
const macro: Macro = {
|
||||
name: `macro-${new Date().toISOString().slice(11, 19)}`,
|
||||
duration: performance.now() - macroState.recStart,
|
||||
frames: macroState.rec.slice(),
|
||||
};
|
||||
macroState.phase = 'idle';
|
||||
macroState.current = macro;
|
||||
notify();
|
||||
return macro;
|
||||
}
|
||||
|
||||
export function startPlaying(macro?: Macro) {
|
||||
const m = macro ?? macroState.current;
|
||||
if (!m || m.frames.length === 0) return;
|
||||
macroState.play = { macro: m, start: performance.now(), idx: 0 };
|
||||
macroState.phase = 'playing';
|
||||
notify();
|
||||
}
|
||||
|
||||
export function stopPlaying() {
|
||||
macroState.play = null;
|
||||
macroState.phase = 'idle';
|
||||
notify();
|
||||
}
|
||||
|
||||
export function tickMacro() {
|
||||
if (macroState.phase === 'recording') {
|
||||
const t = performance.now() - macroState.recStart;
|
||||
// throttle to ~30Hz
|
||||
const last = macroState.rec[macroState.rec.length - 1];
|
||||
if (!last || t - last.t > 33) {
|
||||
macroState.rec.push({
|
||||
t,
|
||||
l: { m: matrixToArray(fakeInput.left.gripWorld), g: fakeInput.left.gripValue, t: fakeInput.left.triggerValue },
|
||||
r: { m: matrixToArray(fakeInput.right.gripWorld), g: fakeInput.right.gripValue, t: fakeInput.right.triggerValue },
|
||||
});
|
||||
}
|
||||
} else if (macroState.phase === 'playing' && macroState.play) {
|
||||
const t = performance.now() - macroState.play.start;
|
||||
const frames = macroState.play.macro.frames;
|
||||
while (
|
||||
macroState.play.idx < frames.length - 1 &&
|
||||
frames[macroState.play.idx + 1].t <= t
|
||||
) {
|
||||
macroState.play.idx++;
|
||||
}
|
||||
const f = frames[macroState.play.idx];
|
||||
if (f) {
|
||||
arrayToMatrix(f.l.m, fakeInput.left.gripWorld);
|
||||
fakeInput.left.gripValue = f.l.g;
|
||||
fakeInput.left.triggerValue = f.l.t;
|
||||
arrayToMatrix(f.r.m, fakeInput.right.gripWorld);
|
||||
fakeInput.right.gripValue = f.r.g;
|
||||
fakeInput.right.triggerValue = f.r.t;
|
||||
}
|
||||
if (t > macroState.play.macro.duration) stopPlaying();
|
||||
}
|
||||
}
|
||||
|
||||
export function isPlaybackActive() {
|
||||
return macroState.phase === 'playing';
|
||||
}
|
||||
|
||||
function matrixToArray(m: THREE.Matrix4): number[] {
|
||||
return Array.from(m.elements);
|
||||
}
|
||||
function arrayToMatrix(arr: number[], target: THREE.Matrix4) {
|
||||
target.fromArray(arr);
|
||||
}
|
||||
|
||||
export function downloadMacro(macro: Macro) {
|
||||
const blob = new Blob([JSON.stringify(macro, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${macro.name}.json`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
export function loadMacroFromFile(file: File): Promise<Macro> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const r = new FileReader();
|
||||
r.onload = () => {
|
||||
try { resolve(JSON.parse(String(r.result)) as Macro); } catch (e) { reject(e); }
|
||||
};
|
||||
r.onerror = reject;
|
||||
r.readAsText(file);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user