The sprite takes the whole image at its pixel size and the texture's name, numbered like any other clash. CreateAssetCommand now accepts starting properties; they go through the schema in execute, so an unknown key or a bad value is refused before the project is touched, and one undo removes the asset. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
117 lines
5.0 KiB
TypeScript
117 lines
5.0 KiB
TypeScript
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 });
|
||
});
|
||
});
|