88 lines
1.9 KiB
TypeScript
88 lines
1.9 KiB
TypeScript
export type InteractableKind = "grab" | "trigger";
|
|
|
|
export interface InteractableHandle {
|
|
kind: InteractableKind;
|
|
label: string;
|
|
onPress: () => void;
|
|
onRelease: () => void;
|
|
}
|
|
|
|
export interface InteractionSnapshot {
|
|
focused: InteractableHandle | null;
|
|
holding: boolean;
|
|
}
|
|
|
|
export class InteractionManager {
|
|
private static _instance: InteractionManager | null = null;
|
|
|
|
private _focused: InteractableHandle | null = null;
|
|
private _holding = false;
|
|
private _holdingHandle: InteractableHandle | null = null;
|
|
private readonly _listeners = new Set<() => void>();
|
|
|
|
static getInstance(): InteractionManager {
|
|
if (!InteractionManager._instance) {
|
|
InteractionManager._instance = new InteractionManager();
|
|
}
|
|
|
|
return InteractionManager._instance;
|
|
}
|
|
|
|
private constructor() {}
|
|
|
|
getState(): InteractionSnapshot {
|
|
return {
|
|
focused: this._focused,
|
|
holding: this._holding,
|
|
};
|
|
}
|
|
|
|
setFocused(handle: InteractableHandle | null): void {
|
|
if (this._focused === handle) return;
|
|
// Never interrupt an active grab via focus change
|
|
if (this._holding) {
|
|
this._focused = handle;
|
|
this._emit();
|
|
return;
|
|
}
|
|
this._focused = handle;
|
|
this._emit();
|
|
}
|
|
|
|
pressInteract(): void {
|
|
if (!this._focused) return;
|
|
|
|
this._holding = this._focused.kind === "grab";
|
|
if (this._holding) this._holdingHandle = this._focused;
|
|
this._focused.onPress();
|
|
this._emit();
|
|
}
|
|
|
|
releaseInteract(): void {
|
|
const handle = this._holdingHandle ?? this._focused;
|
|
if (!handle) return;
|
|
|
|
handle.onRelease();
|
|
this._holding = false;
|
|
this._holdingHandle = null;
|
|
this._emit();
|
|
}
|
|
|
|
subscribe(listener: () => void): () => void {
|
|
this._listeners.add(listener);
|
|
|
|
return () => {
|
|
this._listeners.delete(listener);
|
|
};
|
|
}
|
|
|
|
destroy(): void {
|
|
this._listeners.clear();
|
|
InteractionManager._instance = null;
|
|
}
|
|
|
|
private _emit(): void {
|
|
this._listeners.forEach((cb) => cb());
|
|
}
|
|
}
|