import { describe, expect, it } from 'vitest'; import type { NineSlice } from '../../src/core/model/texture'; import { MAX_TILES, sliceGeometry, sliceKey, sliceSpans } from '../../src/viewport/nineSlice'; const slice = (border: Partial, fill: NineSlice['fill'] = 'stretch'): NineSlice => ({ border: { left: 0, top: 0, right: 0, bottom: 0, ...border }, fill, sourceW: 24, sourceH: 24, scale: 1, }); describe('sliceSpans', () => { it('keeps the corners at their source size and gives the rest to the middle', () => { const spans = sliceSpans(200, 24, 8, 8, 'stretch'); expect(spans.map((s) => [s.p0, s.p1])).toEqual([ [0, 8], [8, 192], [192, 200], ]); // The UVs cut the frame at the same pixels, so the corner shows the corner. expect(spans.map((s) => [s.t0, s.t1])).toEqual([ [0, 1 / 3], [1 / 3, 2 / 3], [2 / 3, 1], ]); }); it('leaves out a part that has no room: a border of zero is no quad at all', () => { expect(sliceSpans(200, 24, 0, 8, 'stretch').map((s) => [s.p0, s.p1])).toEqual([ [0, 192], [192, 200], ]); // Exactly as wide as its two borders: middle gone, the two corners meet. expect(sliceSpans(16, 24, 8, 8, 'stretch').map((s) => [s.p0, s.p1])).toEqual([ [0, 8], [8, 16], ]); }); it('scales both borders down together when the sprite is smaller than they are', () => { const spans = sliceSpans(8, 24, 8, 8, 'stretch'); // Never overlapping corners: 4 + 4 rather than 8 + 8 on a box of 8. expect(spans.map((s) => [s.p0, s.p1])).toEqual([ [0, 4], [4, 8], ]); // The UVs are untouched by the squeeze: each corner still shows its own eight pixels. expect(spans.map((s) => [s.t0, s.t1])).toEqual([ [0, 1 / 3], [2 / 3, 1], ]); }); it('repeats the middle in whole tiles of the frame and cuts the last one', () => { const spans = sliceSpans(50, 24, 8, 8, 'repeat'); const middle = spans.slice(1, -1); expect(middle.map((s) => [s.p0, s.p1])).toEqual([ [8, 16], [16, 24], [24, 32], [32, 40], [40, 42], ]); // The cut tile shows only as much of the picture as it covers – a quarter of the eight pixels. expect(middle.at(-1)?.t1).toBeCloseTo(1 / 3 + (2 / 8) * (1 / 3), 10); // Every tile starts at the middle's own first pixel; none samples past the frame. expect(middle.every((s) => s.t0 === 1 / 3 && s.t1 <= 2 / 3)).toBe(true); }); it('stretches rather than building a tile for every pixel of a huge sprite', () => { // A middle of 14 source pixels: exactly `MAX_TILES` of them still tile, one more stretches. const many = sliceSpans(10 + MAX_TILES * 14, 24, 5, 5, 'repeat'); expect(many.length).toBe(2 + MAX_TILES); const tooMany = sliceSpans(10 + (MAX_TILES + 1) * 14, 24, 5, 5, 'repeat'); expect(tooMany.length).toBe(3); }); it('answers nothing for a box or a picture of no size', () => { expect(sliceSpans(0, 24, 8, 8, 'stretch')).toEqual([]); expect(sliceSpans(200, 0, 8, 8, 'stretch')).toEqual([]); expect(sliceSpans(200, 24, 8, 8, 'stretch', 0)).toEqual([]); }); it('draws the borders scaled, and samples the same pixels for them (#96)', () => { const spans = sliceSpans(200, 24, 8, 8, 'stretch', 2); expect(spans.map((s) => [s.p0, s.p1])).toEqual([ [0, 16], [16, 184], [184, 200], ]); // Bigger on screen, not more of the picture: the corner still shows its eight pixels. expect(spans.map((s) => [s.t0, s.t1])).toEqual([ [0, 1 / 3], [1 / 3, 2 / 3], [2 / 3, 1], ]); // Below 1 the borders shrink the same way. expect(sliceSpans(200, 24, 8, 8, 'stretch', 0.5)[0]).toMatchObject({ p0: 0, p1: 4 }); }); it('scales the repeated tile with the borders, so the pattern keeps its proportion', () => { const middle = sliceSpans(80, 24, 8, 8, 'repeat', 2).slice(1, -1); // A middle of 48 units in tiles of 8 × 2 = 16. expect(middle.map((s) => [s.p0, s.p1])).toEqual([ [16, 32], [32, 48], [48, 64], ]); }); it('squeezes scaled borders like unscaled ones when the sprite is too small for them', () => { // 16 + 16 on a box of 20: both shrink to 10, the middle is gone. expect(sliceSpans(20, 24, 8, 8, 'stretch', 2).map((s) => [s.p0, s.p1])).toEqual([ [0, 10], [10, 20], ]); }); }); describe('sliceGeometry', () => { it('builds one quad per part, in the 0..1 box the unit quad covers', () => { const { positions, uvs, indices } = sliceGeometry( slice({ left: 8, top: 8, right: 8, bottom: 8 }), 200, 200, ); expect(indices.length).toBe(9 * 6); expect(positions.length).toBe(9 * 8); expect(uvs.length).toBe(positions.length); // Fractions, not units: the sprite's size lives in the mesh's scale. expect(Math.min(...positions)).toBe(0); expect(Math.max(...positions)).toBe(1); // The first quad is the top-left corner: 8 of 200 units, showing 8 of 24 pixels. // Close, not equal: the buffers are Float32, where neither 0.04 nor a third is exact. expect(positions[2]).toBeCloseTo(0.04, 6); expect(uvs[2]).toBeCloseTo(1 / 3, 6); expect([positions[0], positions[1], positions[3]]).toEqual([0, 0, 0]); }); it('drops the rows and columns that have no room, keeping the quads rectangular', () => { const { indices } = sliceGeometry(slice({ left: 8, right: 8 }), 200, 200); // Three columns, one row – nothing is cut horizontally. expect(indices.length).toBe(3 * 6); }); }); describe('sliceKey', () => { it('changes with anything the geometry is built from, and not with a sub-pixel of size', () => { const base = slice({ left: 8, right: 8 }); expect(sliceKey(base, 200, 100)).toBe(sliceKey(base, 200.1, 100)); expect(sliceKey(base, 200, 100)).not.toBe(sliceKey(base, 201, 100)); expect(sliceKey(base, 200, 100)).not.toBe(sliceKey(slice({ left: 8, right: 7 }), 200, 100)); expect(sliceKey(base, 200, 100)).not.toBe(sliceKey(slice({ left: 8, right: 8 }, 'repeat'), 200, 100)); expect(sliceKey(base, 200, 100)).not.toBe(sliceKey({ ...base, scale: 2 }, 200, 100)); }); });