feat: build procedural lifecycle-safe drive scene

This commit is contained in:
2026-09-02 04:53:55 +03:00
parent cc0887fee0
commit fd8347fbdc
14 changed files with 1822 additions and 0 deletions

130
src/scene/resources.ts Normal file
View File

@@ -0,0 +1,130 @@
export type SceneResourceKind =
| 'geometry'
| 'material'
| 'texture'
| 'target'
| 'pass'
| 'renderer'
| 'other';
export interface DisposableResource {
dispose(): void;
}
export interface DisconnectableResource {
disconnect(): void;
}
export interface SceneResourceSnapshot {
disposed: boolean;
owned: Readonly<Record<SceneResourceKind, number>>;
listenerCount: number;
observerCount: number;
timerCount: number;
rafCount: 0 | 1;
}
const resourceKinds: readonly SceneResourceKind[] = [
'geometry', 'material', 'texture', 'target', 'pass', 'renderer', 'other',
];
export class SceneResourceBag {
private disposed = false;
private readonly resources = new Map<DisposableResource, SceneResourceKind>();
private readonly listeners = new Set<() => void>();
private readonly observers = new Set<DisconnectableResource>();
private readonly timers = new Map<number, (id: number) => void>();
private raf: { id: number; cancel: (id: number) => void } | null = null;
own<T extends DisposableResource>(kind: SceneResourceKind, resource: T): T {
if (this.disposed) {
resource.dispose();
return resource;
}
if (!this.resources.has(resource)) this.resources.set(resource, kind);
return resource;
}
listen(
target: EventTarget,
type: string,
listener: EventListenerOrEventListenerObject,
options?: AddEventListenerOptions | boolean,
): () => void {
if (this.disposed) return () => undefined;
target.addEventListener(type, listener, options);
let active = true;
const remove = (): void => {
if (!active) return;
active = false;
target.removeEventListener(type, listener, options);
this.listeners.delete(remove);
};
this.listeners.add(remove);
return remove;
}
observe<T extends DisconnectableResource>(observer: T): T {
if (this.disposed) observer.disconnect();
else this.observers.add(observer);
return observer;
}
timer(id: number, clear: (id: number) => void): number {
if (this.disposed) clear(id);
else this.timers.set(id, clear);
return id;
}
releaseTimer(id: number): void {
this.timers.delete(id);
}
animationFrame(id: number, cancel: (id: number) => void): number {
if (this.disposed) {
cancel(id);
return id;
}
if (this.raf) this.raf.cancel(this.raf.id);
this.raf = { id, cancel };
return id;
}
releaseAnimationFrame(id: number): void {
if (this.raf?.id === id) this.raf = null;
}
cancelAnimationFrame(): void {
if (!this.raf) return;
const current = this.raf;
this.raf = null;
current.cancel(current.id);
}
dispose(): void {
if (this.disposed) return;
this.disposed = true;
this.cancelAnimationFrame();
for (const [id, clear] of this.timers) clear(id);
this.timers.clear();
for (const remove of [...this.listeners]) remove();
this.listeners.clear();
for (const observer of this.observers) observer.disconnect();
this.observers.clear();
for (const resource of this.resources.keys()) resource.dispose();
this.resources.clear();
}
snapshot(): SceneResourceSnapshot {
const owned = Object.fromEntries(resourceKinds.map((kind) => [kind, 0])) as Record<SceneResourceKind, number>;
for (const kind of this.resources.values()) owned[kind] += 1;
return {
disposed: this.disposed,
owned,
listenerCount: this.listeners.size,
observerCount: this.observers.size,
timerCount: this.timers.size,
rafCount: this.raf ? 1 : 0,
};
}
}