import { describe, expect, it } from 'vitest'; import { CreateTextureCommand } from '../../src/core/commands/assetCommands'; import { AddFrameCommand } from '../../src/core/commands/frameCommands'; import type { PropertyBag } from '../../src/core/model/Properties'; import { fitAxisToAspect, matchesAspect, matchesPixelSize, spriteFromTexture, texturePixelSize, } from '../../src/core/properties/aspect'; import { makeSpriteScene } from '../helpers'; const FILE = `${'a1b2c3d4'.repeat(8)}.png`; /** A project with a 64 × 48 sheet carrying one 30 × 10 frame. */ function makeSheet(width = 64, height = 48) { const fixture = makeSpriteScene(); const texture = new CreateTextureCommand('Sheet', { file: FILE, width, height }); fixture.stack.execute(texture); const frame = new AddFrameCommand(texture.asset.id, { x: 2, y: 4, w: 30, h: 10 }); fixture.stack.execute(frame); return { ...fixture, textureId: texture.asset.id, frameId: frame.frameId }; } const props = (bag: PropertyBag): PropertyBag => ({ width: 100, height: 100, ...bag }); describe('texturePixelSize', () => { it('is the whole image for a texture id and the rectangle for a frame id', () => { const sheet = makeSheet(); expect(texturePixelSize(sheet.project, props({ texture: sheet.textureId }))).toEqual({ w: 64, h: 48 }); expect(texturePixelSize(sheet.project, props({ texture: sheet.frameId }))).toEqual({ w: 30, h: 10 }); }); it('is null when the sprite wears nothing, or an id that no longer resolves', () => { const sheet = makeSheet(); expect(texturePixelSize(sheet.project, props({ texture: '' }))).toBeNull(); expect(texturePixelSize(sheet.project, props({}))).toBeNull(); expect(texturePixelSize(sheet.project, props({ texture: 'gone' }))).toBeNull(); }); it('is null while the image has not been measured, so no ratio is invented', () => { const sheet = makeSheet(0, 0); expect(texturePixelSize(sheet.project, props({ texture: sheet.textureId }))).toBeNull(); }); }); describe('fitAxisToAspect', () => { const size = { w: 64, h: 48 }; it('takes the other side as given and returns the proportional one', () => { expect(fitAxisToAspect(size, props({ height: 60 }), 'width')).toBe(80); expect(fitAxisToAspect(size, props({ width: 80 }), 'height')).toBe(60); }); it('returns null when the side it would read from is unusable', () => { expect(fitAxisToAspect(size, props({ height: 0 }), 'width')).toBeNull(); expect(fitAxisToAspect(size, props({ height: -5 }), 'width')).toBeNull(); expect(fitAxisToAspect(size, props({ width: Number.NaN }), 'height')).toBeNull(); }); }); describe('matchesPixelSize', () => { const size = { w: 64, h: 48 }; it('asks for both sides, unlike the proportions', () => { expect(matchesPixelSize(size, props({ width: 64, height: 48 }))).toBe(true); // Proportional, but half the size: the "set to 64 × 48 px" action still has something to do. expect(matchesPixelSize(size, props({ width: 32, height: 24 }))).toBe(false); expect(matchesAspect(size, props({ width: 32, height: 24 }))).toBe(true); }); }); describe('matchesAspect', () => { it('compares proportions, not sizes', () => { const size = { w: 64, h: 48 }; expect(matchesAspect(size, props({ width: 64, height: 48 }))).toBe(true); expect(matchesAspect(size, props({ width: 8, height: 6 }))).toBe(true); expect(matchesAspect(size, props({ width: 100, height: 100 }))).toBe(false); }); it('holds after fitting one side, whatever the division left behind', () => { // Measured, not assumed: of the 40 000 size pairs up to 200 × 200, 3135 do not survive the // round trip through a division. 1 × 11 is one of them, and an exact comparison would call // the sprite this very button just fitted not proportional – leaving the button standing. const size = { w: 1, h: 11 }; const width = fitAxisToAspect(size, props({ height: 100 }), 'width')!; expect(width * size.h).not.toBe(100 * size.w); expect(matchesAspect(size, props({ width, height: 100 }))).toBe(true); const height = fitAxisToAspect(size, props({ width }), 'height')!; expect(height).not.toBe(100); expect(matchesAspect(size, props({ width, height }))).toBe(true); }); it('is false for a degenerate sprite rather than dividing by zero', () => { const size = { w: 64, h: 48 }; expect(matchesAspect(size, props({ width: 0, height: 0 }))).toBe(false); expect(matchesAspect(size, props({ width: Number.NaN, height: 10 }))).toBe(false); }); }); describe('spriteFromTexture', () => { it('wears the texture at its pixel size', () => { const sheet = makeSheet(); expect(spriteFromTexture(sheet.project, sheet.textureId)).toEqual({ texture: sheet.textureId, width: 64, height: 48, }); }); it('leaves the size to the defaults while the image has not been measured', () => { const sheet = makeSheet(0, 0); expect(spriteFromTexture(sheet.project, sheet.textureId)).toEqual({ texture: sheet.textureId }); }); });