Files
feuerwerk/tests/core/aspect.test.ts
T
rootandClaude Opus 5 9212e61c4c Create a sprite asset from a texture's context menu
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>
2026-09-11 09:57:19 +02:00

117 lines
5.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 });
});
});