Files
feuerwerk/tests/viewport/texture-fill.test.ts
T
rootandClaude Opus 5 4d84231b9f Trim needless comments and fix the picker width
Remove comments that only restate the code, keeping the section banners
and the notes that carry a reason, a measured fact or an external
contract.

ButtonOptions.width claimed to be a fixed width but only set minWidth,
so the Graph and Texture pickers grew with the name they showed. It is
now what it promises, with a separate minWidth for the buttons that have
two forms. Narrow buttons shrink their padding, otherwise a 24px icon
button had 8px left for its glyph and truncate dropped it entirely.

__gm.labelsDropped() guards that class: the overflow guard only saw text
that was too wide, never a label truncated to nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 15:52:05 +02:00

95 lines
3.8 KiB
TypeScript

/**
* The fill matrix that makes a sprite wear one frame of a sheet. Pinned against the identity
* `uv = inverse(matrix) ∘ normalise(shapeBounds)` rather than against the formula it uses, so
* the checks invert the matrix themselves and ask where the shape's corners land in the image.
*/
import { describe, expect, it } from 'vitest';
import type { FrameRect } from '../../src/core/model/texture';
import { frameFillMatrix, type MatrixValues } from '../../src/viewport/textureFill';
/**
* Where the normalised shape point `(u, v)` samples the image: Pixi's `inverse(style.matrix)`,
* written out for a general affine matrix so the test does not assume an axis-aligned one.
*/
function sampleAt(m: MatrixValues, u: number, v: number): { u: number; v: number } {
const det = m.a * m.d - m.b * m.c;
const dx = u - m.tx;
const dy = v - m.ty;
return { u: (m.d * dx - m.c * dy) / det, v: (m.a * dy - m.b * dx) / det };
}
/** Asserts that the shape's four corners sample exactly the four corners of `rect`. */
function expectCovers(m: MatrixValues, rect: FrameRect, sourceWidth: number, sourceHeight: number): void {
const left = rect.x / sourceWidth;
const right = (rect.x + rect.w) / sourceWidth;
const top = rect.y / sourceHeight;
const bottom = (rect.y + rect.h) / sourceHeight;
for (const [u, v, expectedU, expectedV] of [
[0, 0, left, top],
[1, 0, right, top],
[0, 1, left, bottom],
[1, 1, right, bottom],
] as const) {
const got = sampleAt(m, u, v);
expect(got.u).toBeCloseTo(expectedU, 12);
expect(got.v).toBeCloseTo(expectedV, 12);
}
}
/**
* Number by number rather than `toEqual`: a frame on the top or left edge produces a negative
* zero, which `toEqual` counts as a different value.
*/
function expectMatrix(m: MatrixValues, expected: MatrixValues): void {
for (const key of ['a', 'b', 'c', 'd', 'tx', 'ty'] as const) expect(m[key]).toBeCloseTo(expected[key], 12);
}
describe('frameFillMatrix', () => {
it('declines when the image size is unknown, so the sprite keeps showing the whole picture', () => {
const rect = { x: 0, y: 0, w: 32, h: 32 };
// What an upload that has not reported its size yet looks like in the model.
expect(frameFillMatrix(rect, 0, 0)).toBeNull();
expect(frameFillMatrix(rect, 64, 0)).toBeNull();
expect(frameFillMatrix(rect, -64, 64)).toBeNull();
});
it('declines for a frame with no area, which cannot be spread over anything', () => {
expect(frameFillMatrix({ x: 0, y: 0, w: 0, h: 32 }, 64, 64)).toBeNull();
expect(frameFillMatrix({ x: 0, y: 0, w: 32, h: 0 }, 64, 64)).toBeNull();
});
it('maps the shape onto the top-right quadrant of a four-quadrant sheet', () => {
const rect = { x: 32, y: 0, w: 32, h: 32 };
const m = frameFillMatrix(rect, 64, 64)!;
// Half the sheet across: stretched twofold, shifted by one shape-width to the midpoint.
expectMatrix(m, { a: 2, b: 0, c: 0, d: 2, tx: -1, ty: 0 });
expectCovers(m, rect, 64, 64);
});
it('stretches the axes independently for a non-square frame', () => {
const rect = { x: 0, y: 0, w: 16, h: 64 };
const m = frameFillMatrix(rect, 128, 64)!;
expectMatrix(m, { a: 8, b: 0, c: 0, d: 1, tx: 0, ty: 0 });
expectCovers(m, rect, 128, 64);
});
it('offsets in frame-widths, not in pixels or UV', () => {
// Pixels, UV and frame-widths all differ here, so an offset in the wrong unit is caught.
const rect = { x: 48, y: 16, w: 16, h: 8 };
const m = frameFillMatrix(rect, 256, 128)!;
expectMatrix(m, { a: 16, b: 0, c: 0, d: 16, tx: -3, ty: -2 });
expectCovers(m, rect, 256, 128);
});
it('is the identity for a frame covering the whole sheet', () => {
const rect = { x: 0, y: 0, w: 200, h: 100 };
const m = frameFillMatrix(rect, 200, 100)!;
expectMatrix(m, { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 });
expectCovers(m, rect, 200, 100);
});
});