The object queries only ever returned one object: Object by name, Child by name and Find all stop at the first hit. Getting every coin, every child or every item that passes a test needed a For each with its own bookkeeping. These nodes return the whole set as a list of Object, so For each and Find can use item straight away. - Objects by name returns every scene object with that name in scene order. - Children by name returns every match below object, nearer levels first, the same order as the singular node. - Children returns the direct children, with recursive all of them level by level. - Objects of asset returns every instance of an object asset or container by the same rule as Is asset, spawned ones included. - All four take a snapshot. Destroyed objects are never in it, disabled ones are, as with the singular nodes. - Find has a Mode setting: First, All, Any and Count. First is the default and hides the row, so existing graphs look the same. - Match follows the mode of its paired Find: done with items and indices, done with any, or done with count. An unpaired Match shows First, since the pairing only comes from the wiring. - First and Any stop at the first hit, All and Count walk the list. - Changing the mode drops edges at outputs the Match no longer has, in one undo step. SetNodeParamCommand now cleans the whole graph, but only edges that were valid before the change. - The palette entry "filter" adds a Find set to All with its Match. Found on the way: - After adding the filter entry, "find" + Enter picked Filter. The entry has node type Find, counted as an exact hit, and the tie went to the alphabet. The search now ranks by the entry id. e2e 6z-v caught it. Tests: - ./check.sh: 188 files, 2746 tests, CHECK EXIT 0. - find-modes.test.ts with 11 tests, 11 catalog cases for the four nodes, the asset input guard now expects 3. - e2e 6z-vii with 3 checks: filter from the palette, Count picked in the inspector drops the items edge, a run logs 2, 2, 2, 3, 4, "0, 2". - GM_STEPS=1-6: all passed, 278 checks. - Counter-checks fail as expected: every mode stopping early (2), Match ignoring the mode (6), cleanup on the own node only (1), no recursion (2), no inOrder (2), old ranking (1). In the browser a Match ignoring the mode fails both editor checks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
136 lines
4.9 KiB
TypeScript
136 lines
4.9 KiB
TypeScript
/**
|
|
* The two pure halves of the `asset` property kind: how a stored id reads, and which assets a
|
|
* picker may offer for it. Used by the inspector row and the graph node view alike.
|
|
*/
|
|
import { describe, expect, it } from 'vitest';
|
|
import { CreateAssetCommand } from '../../src/core/commands/assetCommands';
|
|
import { BUILT_IN_NODES } from '../../src/core/graph/nodes';
|
|
import type { AssetType } from '../../src/core/model/AssetTypes';
|
|
import type { Project } from '../../src/core/model/Project';
|
|
import { assetChoiceLabel, assetRefChoices, assetRefLabel } from '../../src/core/properties/assetRef';
|
|
import { makeProject, type ProjectFixture } from '../helpers';
|
|
|
|
function add(fixture: ProjectFixture, type: AssetType, name: string): string {
|
|
const command = new CreateAssetCommand(type, name);
|
|
fixture.stack.execute(command);
|
|
return command.asset.id;
|
|
}
|
|
|
|
/** A project holding one asset of every type, each named after its type. */
|
|
function oneOfEach(): { project: Project; ids: Record<AssetType, string> } {
|
|
const fixture = makeProject();
|
|
const types: AssetType[] = [
|
|
'scene',
|
|
'camera',
|
|
'sprite',
|
|
'text',
|
|
'behavior',
|
|
'collider',
|
|
'container',
|
|
'texture',
|
|
];
|
|
const ids = Object.fromEntries(types.map((type) => [type, add(fixture, type, type)])) as Record<
|
|
AssetType,
|
|
string
|
|
>;
|
|
return { project: fixture.project, ids };
|
|
}
|
|
|
|
describe('assetRefLabel', () => {
|
|
it('reads as the referenced asset name', () => {
|
|
const fixture = makeProject();
|
|
const id = add(fixture, 'sprite', 'Coin');
|
|
expect(assetRefLabel(fixture.project, id)).toBe('Coin');
|
|
});
|
|
|
|
it('reads as "None" when nothing is referenced', () => {
|
|
expect(assetRefLabel(makeProject().project, '')).toBe('None');
|
|
});
|
|
|
|
it('keeps showing the id of a reference that no longer resolves', () => {
|
|
expect(assetRefLabel(makeProject().project, 'gone')).toBe('gone?');
|
|
});
|
|
|
|
it('marks a deleted asset as dangling rather than falling back to "None"', () => {
|
|
const fixture = makeProject();
|
|
const id = add(fixture, 'sprite', 'Coin');
|
|
fixture.stack.undo();
|
|
expect(assetRefLabel(fixture.project, id)).toBe(`${id}?`);
|
|
});
|
|
|
|
it('dangles without a project, but the empty reference is still "None"', () => {
|
|
expect(assetRefLabel(null, 'x')).toBe('x?');
|
|
expect(assetRefLabel(null, '')).toBe('None');
|
|
});
|
|
});
|
|
|
|
describe('assetRefChoices', () => {
|
|
it('offers every placeable asset when no types are given', () => {
|
|
const { project } = oneOfEach();
|
|
expect(assetRefChoices(project).map((a) => a.type)).toEqual([
|
|
'behavior',
|
|
'camera',
|
|
'collider',
|
|
'container',
|
|
'sprite',
|
|
'text',
|
|
]);
|
|
});
|
|
|
|
it('never offers a scene or a texture as a placeable asset', () => {
|
|
const { project } = oneOfEach();
|
|
const types = assetRefChoices(project).map((a) => a.type);
|
|
expect(types).not.toContain('scene');
|
|
expect(types).not.toContain('texture');
|
|
});
|
|
|
|
it('offers exactly the given types, placeable or not', () => {
|
|
const { project, ids } = oneOfEach();
|
|
expect(assetRefChoices(project, ['texture']).map((a) => a.id)).toEqual([ids.texture]);
|
|
expect(assetRefChoices(project, ['scene', 'sprite']).map((a) => a.id)).toEqual([ids.scene, ids.sprite]);
|
|
});
|
|
|
|
it('offers nothing when no asset has one of the given types', () => {
|
|
const fixture = makeProject();
|
|
add(fixture, 'sprite', 'Coin');
|
|
expect(assetRefChoices(fixture.project, ['texture'])).toEqual([]);
|
|
});
|
|
|
|
it('sorts by name, not by creation order', () => {
|
|
const fixture = makeProject();
|
|
add(fixture, 'sprite', 'Zebra');
|
|
add(fixture, 'sprite', 'Apple');
|
|
add(fixture, 'text', 'Mango');
|
|
expect(assetRefChoices(fixture.project).map((a) => a.name)).toEqual(['Apple', 'Mango', 'Zebra']);
|
|
});
|
|
|
|
it('sorts namesakes by type so the order does not depend on creation order', () => {
|
|
const fixture = makeProject();
|
|
add(fixture, 'text', 'Coin');
|
|
add(fixture, 'sprite', 'Coin');
|
|
expect(assetRefChoices(fixture.project).map(assetChoiceLabel)).toEqual(['Coin (sprite)', 'Coin (text)']);
|
|
});
|
|
|
|
it('leaves the asset order of the project alone', () => {
|
|
const fixture = makeProject();
|
|
add(fixture, 'sprite', 'Zebra');
|
|
add(fixture, 'sprite', 'Apple');
|
|
assetRefChoices(fixture.project);
|
|
expect(fixture.project.assets.map((a) => a.name)).toEqual(['Zebra', 'Apple']);
|
|
});
|
|
});
|
|
|
|
describe('node asset inputs', () => {
|
|
// An asset input's field uses the asset kind, never an enum with empty options filled in by the inspector.
|
|
it('use the asset kind and offer every placeable asset', () => {
|
|
const params = BUILT_IN_NODES.flatMap((def) => def.inputs)
|
|
.filter((p) => p.key === 'asset')
|
|
.map((p) => p.field!);
|
|
expect(params.map((p) => p.key)).toEqual(['asset', 'asset', 'asset']);
|
|
for (const param of params) {
|
|
expect(param.kind).toBe('asset');
|
|
expect(param.kind === 'asset' && param.assetTypes).toBeUndefined();
|
|
}
|
|
});
|
|
});
|