import { describe, expect, it } from 'vitest';
import { VoxelGrid } from '../../voxel/grid';
import { assertModelValid, decodeModel, encodeModel, resolveTiles, texIdAt, texBytes, validateModel } from '../format';
import type { ModelTexture, VoxelModel } from '../format';
/** Маленькая сетка-фабрика: 2×3×2 с одним цветным вокселем. */
function makeGrid(): VoxelGrid {
const g = new VoxelGrid(2, 3, 2);
g.set(1, 2, 1, 3);
g.set(0, 0, 0, 1);
return g;
}
const PALETTE = { 1: '#4a5340', 3: '#626c4c' };
describe('формат модели (encode/decode/validate)', () => {
it('roundtrip: encode→decode даёт ту же сетку', () => {
const g = makeGrid();
const back = decodeModel(encodeModel(g, PALETTE));
expect(back.sx).toBe(2);
expect(back.sy).toBe(3);
expect(back.sz).toBe(2);
for (let z = 0; z < 2; z++)
for (let y = 0; y < 3; y++)
for (let x = 0; x < 2; x++) expect(back.get(x, y, z)).toBe(g.get(x, y, z));
});
it('encode не мутирует исходную сетку', () => {
const g = makeGrid();
const before = g.count;
encodeModel(g, PALETTE);
expect(g.count).toBe(before);
});
it('validate: валидная модель — пустой список проблем', () => {
const m = encodeModel(makeGrid(), PALETTE);
expect(validateModel(m)).toEqual([]);
});
it('validate: не-объект и плохой размер', () => {
expect(validateModel(null).length).toBeGreaterThan(0);
expect(validateModel('x').length).toBeGreaterThan(0);
const m = encodeModel(makeGrid(), PALETTE);
expect(validateModel({ ...m, size: [0, 3, 2] }).some((p) => p.includes('size'))).toBe(true);
expect(validateModel({ ...m, size: [2, 3] }).some((p) => p.includes('size'))).toBe(true);
});
it('validate: битый base64 и неверная длина данных', () => {
const m = encodeModel(makeGrid(), PALETTE);
expect(validateModel({ ...m, data: 'не base64!!!' }).some((p) => p.includes('base64'))).toBe(true);
expect(validateModel({ ...m, size: [2, 3, 3] }).some((p) => p.includes('байт'))).toBe(true);
});
it('validate: слоты вне 1..255, плохой hex, воксель без слота', () => {
const m = encodeModel(makeGrid(), PALETTE);
expect(validateModel({ ...m, palette: { 0: '#ffffff' } }).some((p) => p.includes('1..255'))).toBe(true);
expect(validateModel({ ...m, palette: { 1: 'красный' } }).some((p) => p.includes('hex'))).toBe(true);
// воксель слота 9, которого нет в палитре
const bad = { ...m, data: btoa(String.fromCharCode(9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)) };
expect(validateModel(bad).some((p) => p.includes('отсутствует в палитре'))).toBe(true);
});
it('assertModelValid кидает с текстом первой проблемы', () => {
const m = encodeModel(makeGrid(), PALETTE);
expect(() => assertModelValid({ ...m, size: [1, 1, 9999] })).toThrow(/size/);
expect(() => assertModelValid(m)).not.toThrow();
});
it('decodeModel бросает на невалидной модели', () => {
expect(() => decodeModel({ size: [2, 2, 2], palette: { 1: '#111111' }, data: 'AAAA' })).toThrow();
});
it('текстурный слой: roundtrip tex/tiles, texIdAt/texBytes/resolveTiles', () => {
const g = makeGrid();
// тайль 1: грань +z — пиксели [слот 3, слот 1, слот 3, слот 3]
const px = (slots: number[]) => btoa(String.fromCharCode(...slots));
const texData = new Uint8Array(12);
texData[9] = 1; // воксель (1,2,1) — тайль 1
const texture: ModelTexture = {
tiles: { 1: { size: 2, faces: [px([1, 1, 1, 1]), px([1, 1, 1, 1]), px([1, 1, 1, 1]), px([1, 1, 1, 1]), px([3, 1, 3, 3]), px([1, 1, 1, 1])] } },
tex: texData,
};
const model = encodeModel(g, PALETTE, texture);
expect(validateModel(model)).toEqual([]);
expect(texIdAt(model, 9)).toBe(1);
expect(texIdAt(model, 0)).toBe(0);
expect(texBytes(model)![9]).toBe(1);
const resolved = resolveTiles(model);
expect(resolved[1]!.size).toBe(2);
expect(resolved[1]!.faces[4]).toEqual(['#626c4c', '#4a5340', '#626c4c', '#626c4c']);
// модель без текстур: texIdAt = 0, resolveTiles пуст
const plain = encodeModel(g, PALETTE);
expect(texIdAt(plain, 9)).toBe(0);
expect(resolveTiles(plain)).toEqual({});
});
it('validate ловит мусор текстурного слоя', () => {
const g = makeGrid();
const px = (slots: number[]) => btoa(String.fromCharCode(...slots));
const faces6 = (s: number[]) => [px(s), px(s), px(s), px(s), px(s), px(s)];
const base = encodeModel(g, PALETTE, {
tiles: { 1: { size: 2, faces: faces6([1, 1, 1, 1]) } },
tex: new Uint8Array(12),
});
// tex без tiles и наоборот
expect(validateModel({ ...base, tiles: undefined }).some((p) => p.includes('tiles отсутствуют'))).toBe(true);
const noTex = encodeModel(g, PALETTE);
expect(validateModel({ ...noTex, tiles: { 1: { size: 2, faces: faces6([1, 1, 1, 1]) } } }).some((p) => p.includes('tex отсутствует'))).toBe(true);
// плохой размер тайля и грань не той длины
expect(validateModel({ ...base, tiles: { 1: { size: 9, faces: faces6([1]) } } }).some((p) => p.includes('1..8'))).toBe(true);
expect(validateModel({ ...base, tiles: { 1: { size: 2, faces: [px([1]), px([1, 1, 1, 1]), px([1, 1, 1, 1]), px([1, 1, 1, 1]), px([1, 1, 1, 1]), px([1, 1, 1, 1])] } } }).some((p) => p.includes('длины'))).toBe(true);
// тексель 0 запрещён и слот не из палитры
expect(validateModel({ ...base, tiles: { 1: { size: 2, faces: faces6([0, 1, 1, 1]) } } }).some((p) => p.includes('запрещён'))).toBe(true);
expect(validateModel({ ...base, tiles: { 1: { size: 2, faces: faces6([7, 1, 1, 1]) } } }).some((p) => p.includes('не в палитре'))).toBe(true);
// tex ссылается на несуществующий тайль
const tex9 = new Uint8Array(12);
tex9[0] = 5;
expect(validateModel({ ...base, tex: btoa(String.fromCharCode(...tex9)) }).some((p) => p.includes('нет в tiles'))).toBe(true);
});
it('validate: валидная модель-тест без текстур не требует tex (регресс)', () => {
const m: VoxelModel = encodeModel(makeGrid(), PALETTE);
expect(m.tiles).toBeUndefined();
expect(validateModel(m)).toEqual([]);
});
});