import { afterEach, describe, expect, it } from 'vitest'; import { CreateAssetCommand, CreateTextureCommand } from '../../src/core/commands/assetCommands'; import type { Asset } from '../../src/core/model/Asset'; import type { PropertyBag } from '../../src/core/model/Properties'; import { generatedKey, generatedPictureOf, generatesPictures, isGeneratedFile, parseGeneratedKey, registerPictureSource, } from '../../src/core/model/generated'; import { findAsset } from '../../src/core/model/Project'; import { resolvePicture } from '../../src/core/model/texture'; import { spriteFromTexture } from '../../src/core/properties/aspect'; import { pictureChoices, textureRefLabel } from '../../src/core/properties/textureRef'; import { makeSpriteScene } from '../helpers'; const FILE = `${'ab'.repeat(32)}.png`; /** * Stands in for the first real generator (#129): an asset type outside the closed list, which is * also what a plugin would bring. The drawing itself is beside the point here – what is measured * is the path a canvas size and a revision take through the model. */ interface DrawnAsset { id: string; type: string; name: string; props: PropertyBag; canvas: { w: number; h: number }; revision: number; } let unregister: (() => void) | null = null; function register(): void { unregister?.(); unregister = registerPictureSource('drawing', (asset) => { const drawn = asset as unknown as DrawnAsset; return { width: drawn.canvas.w, height: drawn.canvas.h, revision: drawn.revision }; }); } afterEach(() => { unregister?.(); unregister = null; }); function sceneWithDrawing(name = 'Logo') { const fixture = makeSpriteScene(); const drawing: DrawnAsset = { id: 'draw-1', type: 'drawing', name, props: {}, canvas: { w: 256, h: 128 }, revision: 1, }; fixture.project.assets.push(drawing as unknown as Asset); register(); return { ...fixture, drawing }; } describe('the key a drawn picture is named by', () => { it('survives being built and taken apart again', () => { expect(generatedKey('a1', 7)).toBe('gen:a1@7'); expect(parseGeneratedKey('gen:a1@7')).toEqual({ assetId: 'a1', revision: '7' }); // A fingerprint is a revision as good as a counter – it cannot be forgotten in a command. expect(parseGeneratedKey(generatedKey('a1', 'c0ffee'))).toEqual({ assetId: 'a1', revision: 'c0ffee' }); }); it('is nothing an uploaded file could be mistaken for', () => { expect(isGeneratedFile(FILE)).toBe(false); expect(isGeneratedFile('')).toBe(false); expect(isGeneratedFile('gen:a1')).toBe(false); expect(isGeneratedFile('gen:@7')).toBe(false); expect(isGeneratedFile('gen:a1@')).toBe(false); expect(isGeneratedFile('a1@7')).toBe(false); }); }); describe('an asset that draws its own picture', () => { it('is no picture at all while nothing is registered for its type', () => { const f = sceneWithDrawing(); unregister?.(); unregister = null; expect(generatesPictures('drawing')).toBe(false); expect(generatedPictureOf(f.drawing as unknown as Asset)).toBeNull(); expect(resolvePicture(f.project, f.drawing.id)).toBeNull(); expect(textureRefLabel(f.project, f.drawing.id)).toBe(`${f.drawing.id}?`); }); it('resolves to its canvas, under the key of its revision', () => { const f = sceneWithDrawing(); expect(resolvePicture(f.project, f.drawing.id)).toEqual({ file: 'gen:draw-1@1', smooth: true, rect: null, sourceWidth: 256, sourceHeight: 128, slice: null, }); }); it('reads as a new picture once the drawing has moved on', () => { const f = sceneWithDrawing(); const before = resolvePicture(f.project, f.drawing.id)?.file; f.drawing.revision = 2; f.drawing.canvas = { w: 64, h: 64 }; const after = resolvePicture(f.project, f.drawing.id); expect(after?.file).not.toBe(before); expect(after?.file).toBe('gen:draw-1@2'); expect([after?.sourceWidth, after?.sourceHeight]).toEqual([64, 64]); }); it('has nothing to show while its canvas has no size', () => { const f = sceneWithDrawing(); f.drawing.canvas = { w: 0, h: 128 }; expect(resolvePicture(f.project, f.drawing.id)).toBeNull(); }); it('takes the filter it is worn with, like an uploaded picture', () => { const f = sceneWithDrawing(); expect(resolvePicture(f.project, f.drawing.id)?.smooth).toBe(true); f.drawing.props.filter = 'nearest'; expect(resolvePicture(f.project, f.drawing.id)?.smooth).toBe(false); }); it('measures a new sprite, as an uploaded picture does', () => { const f = sceneWithDrawing(); expect(spriteFromTexture(f.project, f.drawing.id)).toEqual({ texture: f.drawing.id, width: 256, height: 128, }); }); it('stands among the textures in a texture field, by name and without frames', () => { const f = sceneWithDrawing('Alpha'); const texture = new CreateTextureCommand('Bricks', { file: FILE, width: 64, height: 48 }); f.stack.execute(texture); texture.asset.frames.push({ id: 'f1', name: 'Idle', x: 0, y: 0, w: 32, h: 24 }); f.stack.execute(new CreateAssetCommand('animation', 'Walk')); expect(pictureChoices(f.project).map((c) => [c.id, c.label])).toEqual([ [f.drawing.id, 'Alpha'], [texture.asset.id, 'Bricks'], ['f1', 'Bricks / Idle'], ]); expect(textureRefLabel(f.project, f.drawing.id)).toBe('Alpha'); }); it('is worn by a sprite through the same property an uploaded picture is', () => { const f = sceneWithDrawing(); const sprite = findAsset(f.project, f.spriteId)!; sprite.props.texture = f.drawing.id; expect(resolvePicture(f.project, String(sprite.props.texture))?.file).toBe('gen:draw-1@1'); }); });