Files
feuerwerk/tests/core/aspect.test.ts
T
rootandClaude Opus 5 4568c6581b Add pixel-size fit and drag-to-reorder in the hierarchy
The aspect button now carries two offers, so it becomes a menu: fit one
side to the texture ratio, or set both sides to the exact pixel size as a
single undo step.

Dropping a row on the outer quarter of another row inserts it into that
gap instead of parenting into it, reordering among the siblings there.
Order is the stored order, so ReorderObjectCommand is a splice; equal z
falls back to tree order, which makes this the way to draw one object in
front of another. Rejected on container content like the other structural
edits.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 22:20:46 +02:00

100 lines
4.4 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,
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);
});
});