Images are uploaded to a content-addressed blob endpoint and stored as data/textures/<sha256>.<ext>; the project only keeps the file name. The sprite points at a texture asset through an ordinary property of the new kind `asset`, so instance overrides, "Apply to container", "Reset to asset", undo and `Set property` work without any extra code. The new kind also replaces the ad-hoc asset picker the graph nodes faked as an enum. Import goes through + -> Texture, drag & drop onto the canvas or Ctrl+V. Images wider than 2048px are downscaled client-side and re-encoded to WebP if needed, because nginx caps uploads at 1MB; an image that already fits is uploaded byte for byte so pixel art is never resampled. The sprite colour default changes from red to white, since colour tints the texture multiplicatively and a tint of white shows an image as it is. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
137 lines
5.0 KiB
TypeScript
137 lines
5.0 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. Both are used by the inspector row and by the graph node view, so
|
|
* they are pinned here rather than through a widget.
|
|
*/
|
|
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';
|
|
|
|
/** Adds an asset and returns its id, so a test can reference it. */
|
|
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 params', () => {
|
|
// There used to be a second notion of an asset reference: an enum with empty options that the
|
|
// graph inspector filled in. This keeps the nodes on the real kind.
|
|
it('use the asset kind and offer every placeable asset', () => {
|
|
const params = BUILT_IN_NODES.flatMap((def) => def.params).filter((p) => p.key === 'asset');
|
|
expect(params.length).toBeGreaterThan(0);
|
|
for (const param of params) {
|
|
expect(param.kind).toBe('asset');
|
|
expect(param.kind === 'asset' && param.assetTypes).toBeUndefined();
|
|
}
|
|
});
|
|
});
|