/** * The drawn half of the picture path (#185): everything that keeps a generated picture in memory, * without touching the GPU, so the bookkeeping can be measured without a browser. * * One picture per asset, keyed by `gen:@`. A new revision is a new key, and * making it frees the one the asset had before – that is the whole invalidation rule. The library * owns what a generator hands it: it destroys the picture when the revision moves on. */ import type { Texture } from 'pixi.js'; import type { Asset } from '../core/model/Asset'; import { parseGeneratedKey } from '../core/model/generated'; import { findAsset, type Project } from '../core/model/Project'; /** Draws the picture of one asset, or null while it cannot be drawn. */ export type PictureFactory = (assetId: string) => T | null; export class GeneratedPictures { private readonly kept = new Map(); private factory: PictureFactory | null = null; /** `free` gives a picture back, by key, once its asset has moved on to a newer revision. */ constructor(private readonly free: (picture: T, key: string) => void) {} /** Installs who draws; a session change swaps it, and everything drawn for the old one goes. */ setFactory(factory: PictureFactory | null): void { this.clear(); this.factory = factory; } /** The picture for this key, drawn on the first ask. Null for anything that is not a key. */ get(key: string): T | null { const kept = this.kept.get(key); if (kept) return kept.picture; const parsed = parseGeneratedKey(key); if (!parsed || !this.factory) return null; const picture = this.factory(parsed.assetId); // No picture is not an error and is not remembered: an asset may be drawn a frame later. if (picture === null) return null; this.release(parsed.assetId); this.kept.set(key, { assetId: parsed.assetId, picture }); return picture; } /** What is in memory, without drawing anything. */ peek(key: string): T | null { return this.kept.get(key)?.picture ?? null; } has(key: string): boolean { return this.kept.has(key); } /** The keys held, in the order they were drawn – what a test reads to see a revision go. */ keys(): string[] { return [...this.kept.keys()]; } clear(): void { for (const key of this.keys()) this.drop(key); } /** Every revision of one asset, however old. */ private release(assetId: string): void { for (const [key, held] of this.kept) if (held.assetId === assetId) this.drop(key); } private drop(key: string): void { const held = this.kept.get(key); if (!held) return; this.kept.delete(key); this.free(held.picture, key); } } /** * Who draws an asset type, keyed by type as a plain string for the same reason the readers in * `core/model/generated.ts` are: a plugin may bring a type of its own. */ export type PictureGenerator = (asset: Asset, project: Project) => Texture | null; const generators = new Map(); export function registerPictureGenerator(type: string, draw: PictureGenerator): () => void { generators.set(type, draw); return () => { if (generators.get(type) === draw) generators.delete(type); }; } export function pictureGeneratorFor(type: string): PictureGenerator | null { return generators.get(type) ?? null; } /** * The factory the library runs with: it looks the asset up in whatever project is open at that * moment and hands it to the generator of its type. Nothing is drawn for an asset that is gone. */ export function projectPictureFactory(projectOf: () => Project | null): PictureFactory { return (assetId) => { const project = projectOf(); const asset = project ? findAsset(project, assetId) : null; return project && asset ? (pictureGeneratorFor(asset.type)?.(asset, project) ?? null) : null; }; }