A kind like colour or texture used to be spelled out in five parallel lists, its port type, its empty value, its glyph, its label and its list tag - 22 literal places for texture alone. core/properties/kinds.ts now holds one row per kind and the rest is derived from it, so adding a kind is one row. The five literal nodes collapse into literals.ts, the six kind menus into one helper. Number, Text, Boolean is now the one order everywhere; the variable and port menus used to list Boolean before Text. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
136 lines
5.9 KiB
TypeScript
136 lines
5.9 KiB
TypeScript
/**
|
||
* A list declares nothing about what it holds: every item carries its own kind, read back off the
|
||
* value – and a color or a texture says so, or it would be a plain number or text.
|
||
*/
|
||
import { describe, expect, it } from 'vitest';
|
||
import { SetOverrideCommand } from '../../src/core/commands/sceneObjectCommands';
|
||
import { AddVariableCommand } from '../../src/core/commands/variableCommands';
|
||
import { type PropertyValue, tagged, untag } from '../../src/core/model/Properties';
|
||
import { findAsset } from '../../src/core/model/Project';
|
||
import { deserializeProject } from '../../src/core/store/serialize';
|
||
import { itemKindOf, makeItem, structItemFields } from '../../src/core/properties/listItems';
|
||
import { makeBehavior, makeSpriteScene, place, roundTrip } from '../helpers';
|
||
|
||
/** A behavior with a list variable `stops`, placed once with an override of its own. */
|
||
function withList(value: unknown = [1, 2]) {
|
||
const f = makeSpriteScene();
|
||
const behavior = makeBehavior(f, 'B');
|
||
f.stack.execute(new AddVariableCommand(behavior.id, 'stops', 'list'));
|
||
const asset = findAsset(f.project, behavior.id)!;
|
||
asset.props.stops = value as never;
|
||
const object = place(f, behavior.id, { x: 0, y: 0 });
|
||
f.stack.execute(new SetOverrideCommand(f.sceneId, object, 'stops', [3]));
|
||
return { f, behaviorId: behavior.id, object };
|
||
}
|
||
|
||
describe('the kind of one list item', () => {
|
||
it('is read off the item itself, a color through its tag', () => {
|
||
expect([7, 'a', true, tagged('color', 0xff0000), { x: 1 }].map(itemKindOf)).toEqual([
|
||
'number',
|
||
'text',
|
||
'boolean',
|
||
'color',
|
||
'struct',
|
||
]);
|
||
});
|
||
|
||
it('lets one list hold all of them at once, through a write and a read back', () => {
|
||
const mixed = [7, 'tee', true, tagged('color', 0xff0000), { x: 1, y: 'two' }];
|
||
const { f, behaviorId } = withList(mixed);
|
||
expect(findAsset(roundTrip(f.project), behaviorId)?.props.stops).toEqual(mixed);
|
||
});
|
||
|
||
/** The one kind JSON cannot tell apart from a number, which is why it is tagged at all. */
|
||
it('keeps a color apart from the number beside it', () => {
|
||
const { f, behaviorId } = withList([tagged('color', 0xff0000), 0xff0000]);
|
||
const back = findAsset(roundTrip(f.project), behaviorId)?.props.stops as PropertyValue[];
|
||
expect(back.map(itemKindOf)).toEqual(['color', 'number']);
|
||
expect(back.map(untag)).toEqual([0xff0000, 0xff0000]);
|
||
});
|
||
|
||
/**
|
||
* The second kind JSON cannot tell apart on its own: an asset id is a text like any other. What a
|
||
* sprite wears is one id whether it names a texture, one of its frames or an animation.
|
||
*/
|
||
it('keeps a texture apart from the text beside it, and reads back as its id', () => {
|
||
const { f, behaviorId } = withList([tagged('texture', 'anim-walk'), 'anim-walk']);
|
||
const back = findAsset(roundTrip(f.project), behaviorId)?.props.stops as PropertyValue[];
|
||
expect(back.map(itemKindOf)).toEqual(['texture', 'text']);
|
||
expect(back.map(untag)).toEqual(['anim-walk', 'anim-walk']);
|
||
});
|
||
|
||
it('turns a text into a texture item and back without losing the id', () => {
|
||
expect(makeItem('texture', 'frame-26')).toEqual(tagged('texture', 'frame-26'));
|
||
expect(makeItem('text', tagged('texture', 'frame-26'))).toBe('frame-26');
|
||
});
|
||
|
||
/** A struct item's field is undeclared too, so a texture there keeps its tag, like a color does. */
|
||
it('stays a texture inside a struct item', () => {
|
||
const { f, behaviorId } = withList([{ pic: tagged('texture', 'frame-26'), n: 1 }]);
|
||
const back = findAsset(roundTrip(f.project), behaviorId)?.props.stops as PropertyValue[];
|
||
expect(back).toEqual([{ pic: tagged('texture', 'frame-26'), n: 1 }]);
|
||
expect(structItemFields(back[0]).map((field) => `${field.key}:${field.kind}`)).toEqual([
|
||
'pic:texture',
|
||
'n:number',
|
||
]);
|
||
});
|
||
|
||
it('changes for one item without touching its neighbours', () => {
|
||
const { f, behaviorId } = withList([1, 2]);
|
||
const asset = findAsset(f.project, behaviorId)!;
|
||
const list = asset.props.stops as PropertyValue[];
|
||
asset.props.stops = [makeItem('text', list[0]), list[1]] as never;
|
||
expect(findAsset(roundTrip(f.project), behaviorId)?.props.stops).toEqual(['1', 2]);
|
||
});
|
||
|
||
it('gives a struct item a shape of its own, each field with its own kind', () => {
|
||
const { f, behaviorId } = withList([{ x: 1, label: 'a' }, { x: 2 }]);
|
||
const back = findAsset(roundTrip(f.project), behaviorId)?.props.stops as PropertyValue[];
|
||
expect(back.map((item) => structItemFields(item).map((field) => `${field.key}:${field.kind}`))).toEqual([
|
||
['x:number', 'label:text'],
|
||
['x:number'],
|
||
]);
|
||
});
|
||
|
||
it('drops a list inside a struct item, the one level too deep', () => {
|
||
const { f, behaviorId } = withList([{ x: 1, deep: [1, 2] }]);
|
||
expect(findAsset(roundTrip(f.project), behaviorId)?.props.stops).toEqual([{ x: 1 }]);
|
||
});
|
||
});
|
||
|
||
/**
|
||
* A node parameter meets no schema on the way in – `asStoredValue` is the only rule it passes. It is
|
||
* therefore the one place where the store itself has to know the tags.
|
||
*/
|
||
describe('a list in a node parameter, where nothing is declared', () => {
|
||
const loadParam = (value: unknown) =>
|
||
deserializeProject({
|
||
id: 'p',
|
||
assets: [
|
||
{
|
||
id: 'b',
|
||
type: 'behavior',
|
||
name: 'B',
|
||
graph: { nodes: [{ id: 'n', type: 'List', x: 0, y: 0, params: { value } }], edges: [] },
|
||
},
|
||
],
|
||
});
|
||
const paramOf = (value: unknown) => {
|
||
const asset = findAsset(loadParam(value), 'b');
|
||
return asset?.type === 'behavior' ? asset.graph.nodes[0]?.params.value : undefined;
|
||
};
|
||
|
||
it('keeps a tagged texture an item of its own', () => {
|
||
expect(paramOf([tagged('texture', 'frame-26'), 'frame-26'])).toEqual([
|
||
tagged('texture', 'frame-26'),
|
||
'frame-26',
|
||
]);
|
||
});
|
||
|
||
it('and one inside a struct item as well', () => {
|
||
expect(paramOf([{ pic: tagged('texture', 'frame-26'), n: 1 }])).toEqual([
|
||
{ pic: tagged('texture', 'frame-26'), n: 1 },
|
||
]);
|
||
});
|
||
});
|