/** * Type-safe event emitter — engine-neutral. * * The emitter itself never knew anything about 3D; it only lived in `stage` * because that is where the app lived. Anything that wants to announce state * changes (an app, a board, a manager) can now use it without pulling in the * renderer. */ export type Listener = (data: T) => void; /** * Events every app announces, whatever it renders. * * Engines extend this with their own map; `history-change` is here because * `CommandManager` — also core's — emits it. */ export interface AppEventMap { "history-change": { canUndo: boolean; canRedo: boolean }; } /** * Instantiate with an event map and every `on` / `off` / `emit` is checked * against it at compile time. */ export class EventEmitter> { private listeners = new Map>>(); /** Subscribe; the returned function unsubscribes. */ public on( event: K, listener: Listener, ): () => void { if (!this.listeners.has(event)) { this.listeners.set(event, new Set()); } this.listeners.get(event)?.add(listener as Listener); return () => this.off(event, listener); } public off( event: K, listener: Listener, ): void { const listeners = this.listeners.get(event); if (listeners) { listeners.delete(listener as Listener); if (listeners.size === 0) { this.listeners.delete(event); } } } /** Iterates a copy, so a listener may unsubscribe during dispatch. */ public emit(event: K, data: TMap[K]): void { const listeners = this.listeners.get(event); if (listeners) { for (const listener of new Set(listeners)) { (listener as Listener)(data); } } } public clear(): void { this.listeners.clear(); } }