Three stores, three importers and three pickers that differ only in their type are one of each (#85)
game-maker / gate (push) Successful in 1m33s
game-maker / suite (push) Successful in 5m46s

Refactoring round one: HttpFontStore/HttpSoundStore/HttpTextureStore collapse into a generic
HttpFileStore over a FileStore contract; fontImport/soundImport/textureImport share assetImport
with an ImportKind per sort; ImagePicker/FontPicker/SoundPicker sit on FilePicker and describe
only their row. The group/comment drag logic leaves GraphCanvas for annotationDrag.ts (with its
own unit test), declaredRows.ts replaces the duplicated row build in particles.ts and shaders.ts,
and dead code in font/sound/compile/emission/transform/actions is gone. README: asset list,
check count 958, the two-job CI, the Scenes wiki page.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Fable 5.1 committed 2026-09-16 17:38:54 +02:00
1 parent 15bdfdf07a
commit cc6db5f350
42 files changed
+1375 -1650

No files matched your search

+3 -6
View File
@@ -1,5 +1,6 @@
import type { ParticlesAsset } from '../core/model/Asset';
import { compileParticles, type ParticlesBuild, particlesFingerprint } from '../core/particles/compile';
import { graphFingerprint } from '../core/graph/Graph';
import { compileParticles, type ParticlesBuild } from '../core/particles/compile';
import { gameClock } from '../core/time';
/**
@@ -22,12 +23,8 @@ export class ParticleLibrary {
this.frozenAt = at;
}
get frozen(): number | null {
return this.frozenAt;
}
buildFor(asset: ParticlesAsset): ParticlesBuild {
const fingerprint = particlesFingerprint(asset.graph);
const fingerprint = graphFingerprint(asset.graph);
const cached = this.builds.get(asset.id);
if (cached && cached.fingerprint === fingerprint) return cached.build;
this.compiles += 1;
+3 -2
View File
@@ -1,6 +1,7 @@
import type { ShaderAsset } from '../core/model/Asset';
import { findAssetOf, type Project } from '../core/model/Project';
import { compileShader, shaderFingerprint, type ShaderBuild } from '../core/shader/compile';
import { graphFingerprint } from '../core/graph/Graph';
import { compileShader, type ShaderBuild } from '../core/shader/compile';
import { gameClock } from '../core/time';
import type { ShaderSource } from './shaders';
@@ -25,7 +26,7 @@ export class ShaderLibrary {
}
buildFor(asset: ShaderAsset): ShaderBuild {
const fingerprint = shaderFingerprint(asset.graph);
const fingerprint = graphFingerprint(asset.graph);
const cached = this.builds.get(asset.id);
if (cached && cached.fingerprint === fingerprint) return cached.build;
this.compiles += 1;
+44 -47
View File
@@ -110,42 +110,58 @@ function readUniforms(
}
/**
* A shader asset worn by one sprite mesh: the fragment replaces Pixi's in the sprite's own draw,
* so there is no filter and no extra pass (#77). The program is shared (`GlProgram.from` caches
* by source), the uniforms are this sprite's; `read` takes them from the item's bag.
* A compiled graph as a Pixi program. The program is shared (`GlProgram.from` caches by source),
* the uniforms are this wearer's; `read` takes them from the item's bag. `resources` are the slots
* Pixi fills for the vertex stage in question – `Shader.resources` only accepts the slots its
* constructor declared, so a subclass names them all up front.
*/
class GraphShader extends Shader {
abstract class CompiledGraphShader extends Shader {
private readonly uniforms: Record<string, number> | null;
private readonly compiled: CompiledShader;
readonly fragment: string;
/** What `Mesh.shader` asks of a shader; kept in step with the mesh's own by `wear`. */
texture: Texture = Texture.WHITE;
constructor(
readonly assetId: string,
compiled: CompiledShader,
program: { vertex: string; name: string },
resources: Record<string, unknown>,
) {
const group = uniformGroupOf(compiled);
super({
glProgram: GlProgram.from({
vertex: SPRITE_VERTEX,
fragment: compiled.fragment,
name: `shader-${assetId}`,
}),
resources: {
uTexture: Texture.WHITE.source,
uSampler: Texture.WHITE.source.style,
...(group ? { shaderUniforms: group } : {}),
},
glProgram: GlProgram.from({ vertex: program.vertex, fragment: compiled.fragment, name: program.name }),
resources: { ...resources, ...(group ? { shaderUniforms: group } : {}) },
});
this.fragment = compiled.fragment;
this.compiled = compiled;
this.uniforms = group ? group.uniforms : null;
}
/** True while this is still the shader `props` ask for, so it can be updated instead of rebuilt. */
wears(id: string, compiled: CompiledShader): boolean {
return this.assetId === id && this.fragment === compiled.fragment;
}
read(props: PropertyBag, now: number): void {
if (this.uniforms) readUniforms(this.uniforms, this.compiled, props, now);
}
}
/**
* A shader asset worn by one sprite mesh: the fragment replaces Pixi's in the sprite's own draw,
* so there is no filter and no extra pass (#77).
*/
class GraphShader extends CompiledGraphShader {
/** What `Mesh.shader` asks of a shader; kept in step with the mesh's own by `wear`. */
texture: Texture = Texture.WHITE;
constructor(assetId: string, compiled: CompiledShader) {
super(
assetId,
compiled,
{ vertex: SPRITE_VERTEX, name: `shader-${assetId}` },
{ uTexture: Texture.WHITE.source, uSampler: Texture.WHITE.source.style },
);
}
/** Pixi binds a mesh's texture only for its own shader, so a custom one takes it here. */
wear(texture: Texture): void {
@@ -168,7 +184,7 @@ export function syncShader(mesh: Mesh, props: PropertyBag, shaders: ShaderSource
const id = shaderOf(props);
const compiled = id ? shaders.compiled(id) : null;
const worn = wornBy(mesh);
if (worn && compiled && worn.assetId === id && worn.fragment === compiled.fragment) {
if (worn && compiled && worn.wears(id, compiled)) {
worn.read(props, shaders.now);
worn.wear(mesh.texture);
return;
@@ -195,26 +211,15 @@ export function syncShader(mesh: Mesh, props: PropertyBag, shaders: ShaderSource
* The pipe breaks the batch for every field anyway, so unlike a shaded sprite (#77) this costs no
* draw call at all: five hundred shaded particles are the one call five hundred plain ones were.
*/
class ParticleGraphShader extends Shader {
private readonly uniforms: Record<string, number> | null;
private readonly compiled: CompiledShader;
readonly fragment: string;
constructor(
readonly assetId: string,
compiled: CompiledShader,
) {
const group = uniformGroupOf(compiled);
super({
glProgram: GlProgram.from({
vertex: PARTICLE_VERTEX,
fragment: compiled.fragment,
name: `particle-shader-${assetId}`,
}),
// `GlParticleContainerAdaptor` writes the field's texture and the pipe's own uniforms into
// whichever shader the container names – but `Shader.resources` only accepts the slots its
// constructor declared, so both have to be here, empty, for Pixi to fill them.
resources: {
class ParticleGraphShader extends CompiledGraphShader {
constructor(assetId: string, compiled: CompiledShader) {
// `GlParticleContainerAdaptor` writes the field's texture and the pipe's own uniforms into
// whichever shader the container names, so both slots have to exist, empty, for Pixi to fill.
super(
assetId,
compiled,
{ vertex: PARTICLE_VERTEX, name: `particle-shader-${assetId}` },
{
uTexture: Texture.WHITE.source,
uSampler: new TextureStyle({}),
uniforms: {
@@ -223,16 +228,8 @@ class ParticleGraphShader extends Shader {
uRound: { value: 1, type: 'f32' },
uResolution: { value: [0, 0], type: 'vec2<f32>' },
},
...(group ? { shaderUniforms: group } : {}),
},
});
this.fragment = compiled.fragment;
this.compiled = compiled;
this.uniforms = group ? group.uniforms : null;
}
read(props: PropertyBag, now: number): void {
if (this.uniforms) readUniforms(this.uniforms, this.compiled, props, now);
);
}
}
@@ -249,7 +246,7 @@ export function syncParticleShader(
const id = shaderOf(props);
const compiled = id ? shaders.compiled(id) : null;
const worn = wornByField(container);
if (worn && compiled && worn.assetId === id && worn.fragment === compiled.fragment) {
if (worn && compiled && worn.wears(id, compiled)) {
worn.read(props, shaders.now);
return;
}