c6283d492c
The debug control now reflects what it actually gates: the 3D hand model rendering (used by World.tsx to decide whether to show the hand-tracking gloves), not the legacy SVG visualizer. - Debug.ts: rename showHandTrackingSvg → showHandTrackingModel (state, GUI label "Show Model", getter/setter) - World.tsx: gate showHandTrackingGloves on the new toggle and drop the unused HandTrackingGloveHandedness import
369 lines
9.8 KiB
TypeScript
369 lines
9.8 KiB
TypeScript
import GUI from "lil-gui";
|
|
import type { Controller } from "lil-gui";
|
|
import type { CameraMode, SceneMode } from "@/types/debug/debug";
|
|
import type { HandTrackingSource } from "@/types/handTracking/handTracking";
|
|
import { FOG_CONFIG } from "@/data/world/fogConfig";
|
|
import { EventEmitter } from "@/utils/core/EventEmitter";
|
|
import { isDebugEnabled } from "@/utils/debug/isDebugEnabled";
|
|
import { logger } from "@/utils/core/Logger";
|
|
|
|
const DEBUG_CONTROLS_STORAGE_KEY = "la-fabrik-debug-controls";
|
|
|
|
interface StoredDebugControls {
|
|
cameraMode: CameraMode;
|
|
handTrackingSource: HandTrackingSource;
|
|
sceneMode: SceneMode;
|
|
}
|
|
|
|
interface DebugEvents {
|
|
change: void;
|
|
}
|
|
|
|
const DEBUG_FOLDER_ORDER = [
|
|
"Lighting",
|
|
"Dynamic Wind",
|
|
"Environment",
|
|
"Game",
|
|
"Interaction",
|
|
"Hand Tracking",
|
|
"Map",
|
|
"Personnages",
|
|
"Debug",
|
|
] as const;
|
|
|
|
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 isHandTrackingSource(value: unknown): value is HandTrackingSource {
|
|
return value === "browser" || value === "backend";
|
|
}
|
|
|
|
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 }
|
|
: {}),
|
|
...(isHandTrackingSource(parsedValue.handTrackingSource)
|
|
? { handTrackingSource: parsedValue.handTrackingSource }
|
|
: {}),
|
|
...(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 events = new EventEmitter<DebugEvents>();
|
|
private readonly folders = new Map<string, GUI>();
|
|
private readonly folderRefCounts = new Map<string, number>();
|
|
private handTrackingSourceController: Controller | null = null;
|
|
private readonly controls: {
|
|
cameraMode: CameraMode;
|
|
fogEnabled: boolean;
|
|
handTrackingSource: HandTrackingSource;
|
|
showDebugOverlay: boolean;
|
|
showHandTrackingModel: 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",
|
|
fogEnabled: FOG_CONFIG.enabled,
|
|
handTrackingSource: storedControls.handTrackingSource ?? "browser",
|
|
showDebugOverlay: true,
|
|
showHandTrackingModel: false,
|
|
showInteractionSpheres: false,
|
|
showPerf: true,
|
|
sceneMode: storedControls.sceneMode ?? "game",
|
|
};
|
|
|
|
this.gui = this.active ? new GUI({ title: "La Fabrik" }) : null;
|
|
|
|
if (this.gui) {
|
|
this.gui.open();
|
|
|
|
this.gui
|
|
.add(this.controls, "cameraMode", { Player: "player", Debug: "debug" })
|
|
.name("Camera Mode")
|
|
.onChange((value: CameraMode) => {
|
|
this.controls.cameraMode = value;
|
|
this.saveAndEmit();
|
|
});
|
|
|
|
this.gui
|
|
.add(this.controls, "sceneMode", { Game: "game", Physics: "physics" })
|
|
.name("Scene")
|
|
.onChange((value: SceneMode) => {
|
|
this.controls.sceneMode = value;
|
|
this.saveAndEmit();
|
|
});
|
|
|
|
this.gui
|
|
.add(this.controls, "showPerf")
|
|
.name("R3F Perf")
|
|
.onChange((value: boolean) => {
|
|
this.controls.showPerf = value;
|
|
this.emit();
|
|
});
|
|
|
|
this.gui
|
|
.add(this.controls, "showDebugOverlay")
|
|
.name("Debug Overlay")
|
|
.onChange((value: boolean) => {
|
|
this.controls.showDebugOverlay = value;
|
|
this.emit();
|
|
});
|
|
|
|
this.createOrderedFolders();
|
|
|
|
const handTrackingFolder = this.createFolder("Hand Tracking");
|
|
|
|
handTrackingFolder
|
|
?.add(this.controls, "showHandTrackingModel")
|
|
.name("Show Model")
|
|
.onChange((value: boolean) => {
|
|
this.controls.showHandTrackingModel = value;
|
|
this.emit();
|
|
});
|
|
|
|
this.handTrackingSourceController =
|
|
handTrackingFolder
|
|
?.add(this.controls, "handTrackingSource", {
|
|
"Browser JS": "browser",
|
|
Backend: "backend",
|
|
})
|
|
.name("Source")
|
|
.onChange((value: HandTrackingSource) => {
|
|
const previousSource = this.controls.handTrackingSource;
|
|
this.controls.handTrackingSource = value;
|
|
logger.info("HandTracking", "Debug source changed", {
|
|
from: previousSource,
|
|
to: value,
|
|
});
|
|
this.saveAndEmit();
|
|
}) ?? null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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, options?: { open?: boolean }): GUI | null {
|
|
if (!this.gui) return null;
|
|
|
|
const existing = this.folders.get(name);
|
|
|
|
if (existing) {
|
|
const refCount = this.folderRefCounts.get(name) ?? 0;
|
|
|
|
if (refCount > 0) {
|
|
this.folderRefCounts.set(name, refCount + 1);
|
|
return null;
|
|
}
|
|
|
|
this.folderRefCounts.set(name, 1);
|
|
return existing;
|
|
}
|
|
|
|
const folder = this.gui.addFolder(name);
|
|
this.folders.set(name, folder);
|
|
this.folderRefCounts.set(name, 1);
|
|
this.sortFolders();
|
|
|
|
if (options?.open) {
|
|
folder.open();
|
|
} else {
|
|
folder.close();
|
|
}
|
|
|
|
return folder;
|
|
}
|
|
|
|
addFogControl(folder: GUI): void {
|
|
folder
|
|
.add(this.controls, "fogEnabled")
|
|
.name("Fog")
|
|
.onChange((value: boolean) => {
|
|
this.controls.fogEnabled = value;
|
|
this.emit();
|
|
});
|
|
}
|
|
|
|
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 {
|
|
return this.events.on("change", listener);
|
|
}
|
|
|
|
getCameraMode(): CameraMode {
|
|
return this.controls.cameraMode;
|
|
}
|
|
|
|
getSceneMode(): SceneMode {
|
|
return this.controls.sceneMode;
|
|
}
|
|
|
|
getShowDebugOverlay(): boolean {
|
|
return this.active && this.controls.showDebugOverlay;
|
|
}
|
|
|
|
getHandTrackingSource(): HandTrackingSource {
|
|
return this.controls.handTrackingSource;
|
|
}
|
|
|
|
setHandTrackingSource(value: HandTrackingSource): void {
|
|
const previousSource = this.controls.handTrackingSource;
|
|
this.controls.handTrackingSource = value;
|
|
this.handTrackingSourceController?.updateDisplay();
|
|
logger.info("HandTracking", "Settings source changed", {
|
|
from: previousSource,
|
|
to: value,
|
|
});
|
|
this.saveAndEmit();
|
|
}
|
|
|
|
getFogEnabled(): boolean {
|
|
return this.controls.fogEnabled;
|
|
}
|
|
|
|
getShowInteractionSpheres(): boolean {
|
|
return this.controls.showInteractionSpheres;
|
|
}
|
|
|
|
getShowHandTrackingModel(): boolean {
|
|
return this.controls.showHandTrackingModel;
|
|
}
|
|
|
|
setShowHandTrackingModel(value: boolean): void {
|
|
this.controls.showHandTrackingModel = 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.events.emit("change", undefined);
|
|
}
|
|
|
|
private saveAndEmit(): void {
|
|
try {
|
|
window.localStorage.setItem(
|
|
DEBUG_CONTROLS_STORAGE_KEY,
|
|
JSON.stringify({
|
|
cameraMode: this.controls.cameraMode,
|
|
handTrackingSource: this.controls.handTrackingSource,
|
|
sceneMode: this.controls.sceneMode,
|
|
}),
|
|
);
|
|
} catch {
|
|
// Debug persistence is optional; controls still work if storage is blocked.
|
|
}
|
|
|
|
this.emit();
|
|
}
|
|
|
|
private createOrderedFolders(): void {
|
|
for (const folderName of DEBUG_FOLDER_ORDER) {
|
|
this.ensureFolder(folderName);
|
|
}
|
|
}
|
|
|
|
private ensureFolder(name: string): GUI | null {
|
|
if (!this.gui) return null;
|
|
|
|
const existing = this.folders.get(name);
|
|
if (existing) return existing;
|
|
|
|
const folder = this.gui.addFolder(name);
|
|
folder.close();
|
|
this.folders.set(name, folder);
|
|
this.folderRefCounts.set(name, 0);
|
|
this.sortFolders();
|
|
|
|
return folder;
|
|
}
|
|
|
|
private sortFolders(): void {
|
|
if (!this.gui) return;
|
|
|
|
const rootElement = this.gui.domElement.querySelector(".children");
|
|
if (!rootElement) return;
|
|
|
|
const orderedFolders = [...this.folders.entries()].sort(([a], [b]) => {
|
|
const aIndex = DEBUG_FOLDER_ORDER.indexOf(
|
|
a as (typeof DEBUG_FOLDER_ORDER)[number],
|
|
);
|
|
const bIndex = DEBUG_FOLDER_ORDER.indexOf(
|
|
b as (typeof DEBUG_FOLDER_ORDER)[number],
|
|
);
|
|
const safeAIndex = aIndex === -1 ? DEBUG_FOLDER_ORDER.length : aIndex;
|
|
const safeBIndex = bIndex === -1 ? DEBUG_FOLDER_ORDER.length : bIndex;
|
|
|
|
if (safeAIndex !== safeBIndex) return safeAIndex - safeBIndex;
|
|
return a.localeCompare(b);
|
|
});
|
|
|
|
for (const [, folder] of orderedFolders) {
|
|
rootElement.appendChild(folder.domElement);
|
|
}
|
|
}
|
|
}
|