Files
La-Fabrik/src/utils/debug/Debug.ts
T
2026-05-06 23:16:58 +01:00

232 lines
5.8 KiB
TypeScript

import GUI from "lil-gui";
import type { CameraMode, SceneMode } from "@/types/debug/debug";
import { isDebugEnabled } from "@/utils/debug/isDebugEnabled";
const DEBUG_CONTROLS_STORAGE_KEY = "la-fabrik-debug-controls";
interface StoredDebugControls {
cameraMode: CameraMode;
sceneMode: SceneMode;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null;
}
function isCameraMode(value: unknown): value is CameraMode {
return value === "player" || value === "debug";
}
function isSceneMode(value: unknown): value is SceneMode {
return value === "game" || value === "physics";
}
function getStoredDebugControls(): Partial<StoredDebugControls> {
try {
const rawValue = window.localStorage.getItem(DEBUG_CONTROLS_STORAGE_KEY);
if (!rawValue) return {};
const parsedValue: unknown = JSON.parse(rawValue);
if (!isRecord(parsedValue)) return {};
return {
...(isCameraMode(parsedValue.cameraMode)
? { cameraMode: parsedValue.cameraMode }
: {}),
...(isSceneMode(parsedValue.sceneMode)
? { sceneMode: parsedValue.sceneMode }
: {}),
};
} catch {
return {};
}
}
export class Debug {
private static instance: Debug | null = null;
public readonly active: boolean;
private readonly gui: GUI | null;
private readonly folders = new Map<string, GUI>();
private readonly folderRefCounts = new Map<string, number>();
private readonly listeners = new Set<() => void>();
private readonly controls: {
cameraMode: CameraMode;
showDebugOverlay: boolean;
showHandTrackingSvg: boolean;
showInteractionSpheres: boolean;
showPerf: boolean;
sceneMode: SceneMode;
};
static getInstance(): Debug {
if (!Debug.instance) {
Debug.instance = new Debug();
}
return Debug.instance;
}
private constructor() {
this.active = isDebugEnabled();
const storedControls = getStoredDebugControls();
this.controls = {
cameraMode: storedControls.cameraMode ?? "player",
showDebugOverlay: true,
showHandTrackingSvg: false,
showInteractionSpheres: false,
showPerf: true,
sceneMode: storedControls.sceneMode ?? "game",
};
this.gui = this.active ? new GUI({ title: "La-Fabrik Debug" }) : null;
if (this.gui) {
const folder = this.createFolder("Debug");
if (!folder) return;
folder
.add(this.controls, "cameraMode", { Player: "player", Debug: "debug" })
.name("Camera Mode")
.onChange((value: CameraMode) => {
this.controls.cameraMode = value;
this.saveAndEmit();
});
folder
.add(this.controls, "sceneMode", { Game: "game", Physics: "physics" })
.name("Scene")
.onChange((value: SceneMode) => {
this.controls.sceneMode = value;
this.saveAndEmit();
});
folder
.add(this.controls, "showPerf")
.name("R3F Perf")
.onChange((value: boolean) => {
this.controls.showPerf = value;
this.emit();
});
folder
.add(this.controls, "showDebugOverlay")
.name("Debug Overlay")
.onChange((value: boolean) => {
this.controls.showDebugOverlay = value;
this.emit();
});
const handTrackingFolder = this.createFolder("Hand Tracking");
handTrackingFolder
?.add(this.controls, "showHandTrackingSvg")
.name("Afficher SVG")
.onChange((value: boolean) => {
this.controls.showHandTrackingSvg = value;
this.emit();
});
}
}
/**
* Acquires a named GUI folder. Returns the folder on first acquisition and null
* on subsequent acquisitions so callers only register controls once.
*/
createFolder(name: string): GUI | null {
if (!this.gui) return null;
const existing = this.folders.get(name);
if (existing) {
this.folderRefCounts.set(name, (this.folderRefCounts.get(name) ?? 0) + 1);
return null;
}
const folder = this.gui.addFolder(name);
this.folders.set(name, folder);
this.folderRefCounts.set(name, 1);
return folder;
}
destroyFolder(name: string): void {
const folder = this.folders.get(name);
const refCount = this.folderRefCounts.get(name);
if (!folder || refCount === undefined) return;
if (refCount > 1) {
this.folderRefCounts.set(name, refCount - 1);
return;
}
folder.destroy();
this.folders.delete(name);
this.folderRefCounts.delete(name);
}
subscribe(listener: () => void): () => void {
this.listeners.add(listener);
return () => {
this.listeners.delete(listener);
};
}
getCameraMode(): CameraMode {
return this.controls.cameraMode;
}
getSceneMode(): SceneMode {
return this.controls.sceneMode;
}
getShowDebugOverlay(): boolean {
return this.active && this.controls.showDebugOverlay;
}
getShowInteractionSpheres(): boolean {
return this.controls.showInteractionSpheres;
}
getShowHandTrackingSvg(): boolean {
return this.controls.showHandTrackingSvg;
}
setShowHandTrackingSvg(value: boolean): void {
this.controls.showHandTrackingSvg = value;
this.emit();
}
setShowInteractionSpheres(value: boolean): void {
this.controls.showInteractionSpheres = value;
this.emit();
}
getShowPerf(): boolean {
return this.active && this.controls.showPerf;
}
private emit(): void {
this.listeners.forEach((listener) => listener());
}
private saveAndEmit(): void {
try {
window.localStorage.setItem(
DEBUG_CONTROLS_STORAGE_KEY,
JSON.stringify({
cameraMode: this.controls.cameraMode,
sceneMode: this.controls.sceneMode,
}),
);
} catch {
// Debug persistence is optional; controls still work if storage is blocked.
}
this.emit();
}
}