Files
feuerwerk/tests/editor/struct-field-edit.test.ts
T
rootandClaude Fable 5.1 42cafa7543
game-maker / suite (push) Failing after 3m59s
Shader assets: graph-built shaders, the Shader tab and sprite rows from the graph (#62)
A shader is an asset carrying a node graph (Texture color, Output, Property,
Color, Add/Subtract/Multiply/Divide, Mix, Luminance) that compiles to the GLSL
fragment of a Pixi filter. The Shader tab reuses the Behavior canvas; a sprite
wears a shader through its Shader picker, and the rows below it are the
graph's Property nodes. Property schema, resolution and deviation take the
project because a sprite's rows now live in another asset. The built-in
registry of the prototype is gone; "Adjust colors" ships as a graph in the
fixture and the three example games.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 22:41:14 +02:00

124 lines
5.6 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.
/**
* Editing one field of a struct rebuilds the whole bundle – an override is one value per property
* key, not per field. The question here is what the untouched fields are filled from: an override
* is sparse and starts out unset, so only the *resolved* value carries the asset's own numbers.
*/
import { beforeEach, describe, expect, it } from 'vitest';
import { CreateAssetCommand, SetAssetPropertyCommand } from '../../src/core/commands/assetCommands';
import { PlaceAssetCommand, SetOverrideCommand } from '../../src/core/commands/sceneObjectCommands';
import { AddStructFieldCommand, AddVariableCommand } from '../../src/core/commands/variableCommands';
import type { PropertyValue } from '../../src/core/model/Properties';
import type { SceneObject } from '../../src/core/model/SceneObject';
import { createEmptyProject, findAsset, findScene } from '../../src/core/model/Project';
import { requirePropDef } from '../../src/core/commands/require';
import { resolveAssetProps, resolveSceneObjectProps } from '../../src/core/properties/resolve';
import { structFieldsOf } from '../../src/editor/inspector/previewHandlers';
import { EditorState } from '../../src/editor/state';
const FIELDS = [
{ key: 'x', kind: 'number' },
{ key: 'y', kind: 'number' },
{ key: 'tag', kind: 'text' },
] as const;
function setup() {
const state = new EditorState();
state.loadProject(createEmptyProject('p', 'P'), 1);
const scene = new CreateAssetCommand('scene', 'Level');
const behavior = new CreateAssetCommand('behavior', 'B');
// A rejected command only returns false, so an unasserted setup would let the tests below pass
// for the wrong reason.
expect(state.execute(scene)).toBe(true);
expect(state.execute(behavior)).toBe(true);
expect(state.execute(new AddVariableCommand(behavior.asset.id, 'piece', 'struct'))).toBe(true);
for (const field of FIELDS)
expect(state.execute(new AddStructFieldCommand(behavior.asset.id, 'piece', { ...field }))).toBe(true);
// The asset's own values: what the inspector shows for an object that overrides nothing.
expect(
state.execute(new SetAssetPropertyCommand(behavior.asset.id, 'piece', { x: 3, y: 4, tag: 'T' })),
).toBe(true);
const place = new PlaceAssetCommand(scene.asset.id, behavior.asset.id, { x: 0, y: 0 });
expect(state.execute(place)).toBe(true);
return { state, sceneId: scene.asset.id, behaviorId: behavior.asset.id, objectId: place.objectId };
}
describe('editing one field of a struct', () => {
let f: ReturnType<typeof setup>;
beforeEach(() => {
f = setup();
});
const asset = () => findAsset(f.state.project, f.behaviorId)!;
const object = (): SceneObject =>
findScene(f.state.project, f.sceneId)!.objects.find((o) => o.id === f.objectId)!;
const structDef = () => {
const def = requirePropDef(f.state.project, asset(), 'piece');
if (def.kind !== 'struct') throw new Error('piece is not a struct');
return def;
};
/** The spec `objectSection` builds for an override, resolved value included. */
const objectHandlers = () =>
structFieldsOf<SceneObject>({ state: f.state }, structDef(), {
target: object,
read: (o) => o.overrides.piece,
current: (o) => resolveSceneObjectProps(f.state.project, asset(), o).piece,
write: (o, value) => (o.overrides.piece = value),
remove: (o) => delete o.overrides.piece,
commit: (value) => new SetOverrideCommand(f.sceneId, f.objectId, 'piece', value),
});
it('fills the untouched fields of a first override from the asset, not from an empty bundle', () => {
objectHandlers()({ key: 'x', kind: 'number' }).onChange(9);
expect(object().overrides.piece).toEqual({ x: 9, y: 4, tag: 'T' });
});
it('leaves the others alone on a second edit, and undo goes back one field at a time', () => {
objectHandlers()({ key: 'x', kind: 'number' }).onChange(9);
objectHandlers()({ key: 'tag', kind: 'text' }).onChange('hi');
expect(object().overrides.piece).toEqual({ x: 9, y: 4, tag: 'hi' });
f.state.commands.undo();
expect(object().overrides.piece).toEqual({ x: 9, y: 4, tag: 'T' });
f.state.commands.undo();
expect(object().overrides.piece).toBeUndefined();
});
it('builds on the preview while typing, and still undoes to before it', () => {
const handlers = objectHandlers()({ key: 'x', kind: 'number' });
handlers.onInput(1);
handlers.onInput(12);
handlers.onChange(12);
expect(object().overrides.piece).toEqual({ x: 12, y: 4, tag: 'T' });
f.state.commands.undo();
expect(object().overrides.piece).toBeUndefined();
});
it('copies those values in rather than keeping them tied to the asset', () => {
objectHandlers()({ key: 'x', kind: 'number' }).onChange(9);
f.state.execute(new SetAssetPropertyCommand(f.behaviorId, 'piece', { x: 3, y: 99, tag: 'T' }));
// The whole bundle is overridden now, so the asset moving on no longer reaches y.
expect(resolveSceneObjectProps(f.state.project, asset(), object()).piece).toEqual({
x: 9,
y: 4,
tag: 'T',
});
});
it('takes the stored value where that already is the resolved one – the dense asset bag', () => {
const handlers = structFieldsOf<{ props: Record<string, PropertyValue> }>(
{ state: f.state },
structDef(),
{
target: () => asset(),
read: (a) => a.props.piece,
write: (a, value) => (a.props.piece = value),
commit: (value) => new SetAssetPropertyCommand(f.behaviorId, 'piece', value),
},
);
handlers({ key: 'y', kind: 'number' }).onChange(8);
expect(resolveAssetProps(f.state.project, asset()).piece).toEqual({ x: 3, y: 8, tag: 'T' });
});
});