Two compilers that walk a graph the same way walk it through one emitter (#85)
game-maker / suite (push) Failing after 2m22s
game-maker / gate (push) Successful in 2m53s

Refactoring round two: the particle and shader compilers share emitGraph.ts (cycle detection,
duplicate names, emit order) and keep only their rules. FontLibrary and TextureLibrary sit on
FileLibrary (base URL, loading/failed bookkeeping, listeners); SoundLibrary keeps its own
contract. The three Create*Command share ImportFileCommand, the three renames share
RenameCommand - class names stay. bandBounds() in framing.ts and portNameField() in
portSection.ts replace in-file clones, and three comments that told history now state the rule.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5 committed 2026-09-16 19:28:28 +02:00
1 parent bf0be66c9d
commit d3563897d8
16 files changed
+376 -379

No files matched your search

+13 -39
View File
@@ -1,8 +1,7 @@
import { Assets, CanvasSource, Texture } from 'pixi.js';
import type { FrameRect, TextureRef } from '../core/model/texture';
import { type EdgeMode, SHEET_EDGE } from '../core/shader/nodes';
const DEFAULT_BASE = '/game-maker/data/textures';
import { FileLibrary } from './FileLibrary';
/**
* Pixi turns `clamp-to-edge` into `repeat` on every texture a Graphics fill draws with
@@ -22,24 +21,14 @@ const ADDRESS_MODE: Record<EdgeMode, 'repeat' | 'mirror-repeat' | 'clamp-to-edge
clamp: 'clamp-to-edge',
};
/**
* Keeps uploaded images on the GPU, keyed by file name. Nothing is evicted: file names are
* content addressed, so a cached texture can never go stale. A miss is not an error – it starts
* the load and reports "not yet", so callers never await and get a redraw when the picture lands.
*/
export class TextureLibrary {
private base = DEFAULT_BASE;
/** Keeps uploaded images on the GPU; callers never await and get a redraw when the picture lands. */
export class TextureLibrary extends FileLibrary<Texture> {
private readonly loaded = new Map<string, Texture>();
/** Frames cut from their sheets, keyed by file and rectangle; kept like the files. */
private readonly crops = new Map<string, Texture>();
private readonly loading = new Set<string>();
/** Never retried, so one broken reference cannot spam the network. */
private readonly failed = new Set<string>();
private readonly listeners = new Set<() => void>();
/** Where the files are served from. Set once at start-up, before anything draws. */
configure(base: string): void {
this.base = base.replace(/\/+$/, '');
constructor() {
super('/game-maker/data/textures', 'textures');
}
/**
@@ -83,12 +72,6 @@ export class TextureLibrary {
return this.loaded.get(file) ?? null;
}
/** Runs `fn` whenever a picture has arrived; returns the unsubscribe. */
onChange(fn: () => void): () => void {
this.listeners.add(fn);
return () => this.listeners.delete(fn);
}
/**
* A frame as a texture of its own, cut the first time it is asked for. Drawn from the sheet
* through a fill matrix it showed a row of the neighbouring cell along any edge that fell
@@ -110,23 +93,14 @@ export class TextureLibrary {
return texture;
}
private startLoading(file: string): void {
if (this.loading.has(file) || this.failed.has(file)) return;
this.loading.add(file);
void Assets.load<Texture>(`${this.base}/${file}`).then(
(texture) => {
this.loading.delete(file);
texture.source.style.addressMode = EDGE_MODE;
texture.source.style.update();
this.loaded.set(file, texture);
for (const listener of this.listeners) listener();
},
(error: unknown) => {
this.loading.delete(file);
this.failed.add(file);
console.error(`[textures] cannot load ${file}`, error);
},
);
protected fetchFile(url: string): Promise<Texture> {
return Assets.load<Texture>(url);
}
protected keep(file: string, texture: Texture): void {
texture.source.style.addressMode = EDGE_MODE;
texture.source.style.update();
this.loaded.set(file, texture);
}
}