232 lines
5.8 KiB
TypeScript
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();
|
|
}
|
|
}
|