/** * Portable interaction takeover primitive v0.3. * * This module deliberately has no engine or renderer dependency. It owns stack ordering, * validation, lifecycle, results and safety teardown; a trusted host owns the actual input, * camera, locomotion and HUD integrations. */ export type ModeCapability = 'locomotion' | 'cameraControl' | 'defaultHud' | 'interaction'; /** @deprecated Use {@link ModeCapability}. Retained for the v0.2 migration release. */ export type Capability = ModeCapability; export interface Suppression { locomotion?: boolean; cameraControl?: boolean; defaultHud?: boolean; interaction?: boolean; } export interface CameraIntent { target: { x: number; y: number; z: number; }; distance: number; fov?: number; orbit?: { yawDeg: number; pitchDeg: number; }; blendSeconds?: number; clamp?: { yaw?: [number, number]; pitch?: [number, number]; }; } export type StandardModeAction = 'move' | 'look' | 'primary' | 'secondary' | 'tertiary' | 'cancel'; export type TouchPresentationKind = 'button' | 'drag' | 'tap' | 'hold' | 'stick' | 'none'; export type TouchPresentationZone = 'surface' | 'primary' | 'secondary' | 'left' | 'right'; export interface TouchPresentation { kind: TouchPresentationKind; zone: TouchPresentationZone; group?: string; order?: number; } /** @deprecated Use {@link TouchPresentation}. Retained for the v0.2 consumer migration. */ export type TouchBinding = TouchPresentation; /** @deprecated Use {@link TouchPresentationKind}. Retained for the v0.2 consumer migration. */ export type TouchKind = TouchPresentationKind; /** @deprecated v0.3 touch declarations are already normalized presentation objects. */ export declare function normaliseTouch(touch: TouchPresentation): TouchPresentation; export interface VerbBinding { /** The canonical platform action. Modes never declare raw device bindings. */ action: StandardModeAction; label: string; touch: TouchPresentation; continuous?: boolean; repeatMs?: number; disabled?: boolean; disabledReason?: string; /** Verbs in the same non-empty group are mutually exclusive presentation states. */ exclusiveGroup?: string; } export type InputMap = Record; export type VerbPhase = 'start' | 'end' | 'repeat'; export interface VerbEvent { phase: VerbPhase; value?: number; vector?: { x: number; y: number; }; at?: { x: number; y: number; }; heldMs?: number; source: 'key' | 'pointer' | 'touch' | 'gamepad' | 'ui' | 'script'; } export type VerbHandler = (event: VerbEvent, verb: string) => void; export type VerbSink = (verb: string, event: VerbEvent) => void; export declare const ANY_VERB = "*"; export type ModeEndReason = 'released' | 'popped' | 'cleared' | 'error'; export interface ModeResult { value?: R; reason: ModeEndReason; aborted: boolean; error?: unknown; } export interface PushOptions { onResult?(result: ModeResult): void; onEvent?(event: string, payload: unknown): void; } export interface PushOutcome { ok: boolean; error?: unknown; /** Always settles, including when a push is rejected. */ done: Promise>; } export interface ModeContext { readonly params: Record; readonly live: boolean; readonly covered: boolean; readonly aborted: boolean; setCamera(intent: CameraIntent | null): void; setInput(input: InputMap | null): void; onVerb(verb: string | typeof ANY_VERB, handler: VerbHandler): () => void; isVerbActive(verb: string): boolean; emit(event: string, payload?: unknown): void; release(result?: R): void; diag(patch: Record): void; clearDiag(...keys: string[]): void; } export interface InteractionMode { readonly id: string; readonly suppress: Suppression; readonly camera?: CameraIntent; readonly input?: InputMap; /** Covered modes do not tick unless they explicitly opt in. They never regain input. */ readonly updateWhileCovered?: boolean; enter(ctx: ModeContext): void | Promise; exit(ctx: ModeContext): void | Promise; update?(dtSeconds: number, ctx: ModeContext): void; onCovered?(ctx: ModeContext): void; onRevealed?(ctx: ModeContext): void; } export interface ModeHost { setSuppression(effective: Required): void; applyCamera(intent: CameraIntent | null): void; setVerbs(input: InputMap | null, dispatch: VerbSink | null): void; reportDiag(modeId: string, patch: Record): void; onModeEvent?(modeId: string, event: string, payload: unknown): void; onError?(error: unknown, modeId: string, phase: ModePhase): void; } export type ModePhase = 'enter' | 'exit' | 'update' | 'verb' | 'covered' | 'revealed'; export interface InputLeaseRequest { readonly ownerToken: object; readonly actions: readonly StandardModeAction[]; } export interface InputLeaseHandle { readonly epoch: number; readonly enforced: boolean; release(): void; } /** * Validate a mode input map. Suppression is optional for callers validating a reusable map; the * stack always supplies it and therefore enforces the reachable-cancel rule. */ export declare function validateInputMap(input: InputMap, suppression?: Suppression): string[]; export declare class ModeStack { #private; constructor(host: ModeHost); get active(): InteractionMode | null; get depth(): number; get busy(): boolean; has(id: string): boolean; push(mode: InteractionMode, params?: Record, options?: PushOptions): Promise>; pop(id?: string, aborted?: boolean): Promise; clear(aborted?: boolean): Promise; update(dtSeconds: number): void; dispatchVerb(verb: string, event: VerbEvent): boolean; dispose(): Promise; } export type VisibilitySafetyTarget = EventTarget & { readonly hidden?: boolean; }; /** * Bind browser lifecycle edges that otherwise strand held input or camera takeover. * * `visibilitychange` is a Document event (and does not bubble to Window), so its target is * deliberately separate from the window lifecycle target. Tests and non-browser hosts may pass * an explicit visibility target; `null` disables only that binding. */ export declare function bindStackSafety(stack: ModeStack, target?: EventTarget, visibilityTarget?: VisibilitySafetyTarget | null): () => void;