Files
feuerwerk/src/viewport/generatedPictures.ts
T
adminandClaude Opus 5 592ea942d9
game-maker / gate (push) Successful in 2m14s
game-maker / suite (push) Failing after 7m14s
Vector shapes can fill with gradients and textures (#186)
A vector shape could only be filled and outlined with one flat colour.
Shading, glows and patterned surfaces had to be faked with stacks of
shapes. Every shape now has a paint type for its fill and its outline.

- Fill type: solid colour, linear gradient, radial gradient or
  texture. Line type offers the same without texture.
- Colour stops are edited on a bar in the inspector, as agreed in the
  issue. A click on the bar adds a stop in the colour the gradient
  already has there, so adding never changes the picture. At least two
  stops remain.
- The new Paint tool in the tab shows two handles for the selected
  shape: axis start and end, centre and radius, or the corners of one
  texture tile. "Handles: Fill / Outline" picks which paint is edited.
- Moving a whole shape moves its gradients and tile with it. Every
  gesture is one undo step.
- A texture fill always tiles, because Pixi turns a non-repeating
  pattern into a repeating one anyway. When switched on, one tile
  covers the whole shape.
- Only uploaded textures and frames can be a fill, never a drawing.
  A drawing filled with itself would rasterise itself mid-raster.
- The fingerprint of a drawing includes the texture and frame a fill
  points to, so the picture redraws when the image is swapped.

Findings:

- graphics.destroy(true) also destroyed the textures of the fills.
  With a texture fill it deleted the tile slice and the next redraw
  crashed on reading 'scaleMode' of null. It is now destroy().
- The gradient cache destroyed old gradients itself, which gave 20
  Pixi warnings "destroyed while still bound". They are now handed to
  Pixi's texture GC with autoGarbageCollect switched on. The console
  of module 47 is empty again, the last CI log had 0 of them.

Tests:

- ./check.sh: 135 files, 2011 tests, exit 0 (before 134 / 1986)
- npm run build: exit 0
- GM_STEPS=47, freshly seeded: ALL PASSED, 54 checks (before 44),
  0 console warnings
- 25 new unit tests, 10 new e2e checks in block 47i
- Four counter-checks bite, originals restored and verified with cmp
- Check sum 1232 in README and wiki, calculated (1222 + 10)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 13:05:02 +02:00

105 lines
3.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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:<assetId>@<revision>`. 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<T> = (assetId: string) => T | null;
export class GeneratedPictures<T> {
private readonly kept = new Map<string, { assetId: string; picture: T }>();
private factory: PictureFactory<T> | 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<T> | 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<string, PictureGenerator>();
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<Texture> {
return (assetId) => {
const project = projectOf();
const asset = project ? findAsset(project, assetId) : null;
return project && asset ? (pictureGeneratorFor(asset.type)?.(asset, project) ?? null) : null;
};
}