import { describe, expect, it } from 'vitest';
import { mannequin } from '../mannequin';
import { validateRig, poseVoxels } from '../../animation/rig';
import { decodeModel, resolveTiles, texIdAt } from '../format';
describe('mannequin (манекен + рига)', () => {
it('детерминирован', () => {
expect(JSON.stringify(mannequin())).toBe(JSON.stringify(mannequin()));
});
it('ригa проходит числовую валидацию', () => {
const m = mannequin();
expect(validateRig(m.rig, m.model)).toEqual([]);
});
it('состав частей: ноги 6 ×2, руки 6 ×2, корень 140 (торс+голова+детали)', () => {
const { model, rig } = mannequin();
const g = decodeModel(model);
expect(g.count()).toBe(164); // 6×2 ноги + 6×2 руки + 140 корень
const byBone = [0, 0, 0, 0, 0];
for (let i = 0; i < g.data.length; i++)
if (g.data[i] !== 0) byBone[rig.binding[i]]++;
expect(byBone).toEqual([140, 6, 6, 6, 6]);
});
it('лицо — текстурой: тайли 2×2 на фасаде головы, глаза в арте, мыски сапог', () => {
const m = mannequin();
const g = decodeModel(m.model);
const at = (x: number, y: number, z: number) => g.data[x + g.sx * (y + g.sy * z)];
expect(at(3, 0, 3)).toBe(2); // мысок сапога к +Z
expect(at(4, 11, 2)).toBe(7); // шея
// все 16 вокселей фасада головы (z3, y12..15, x3..6) несут тайли
const texId = (x: number, y: number) => texIdAt(m.model, x + g.sx * (y + g.sy * 3));
for (let y = 12; y <= 15; y++)
for (let x = 3; x <= 6; x++) expect(texId(x, y)).toBeGreaterThan(0);
// вне фасада тайлей нет
expect(texIdAt(m.model, 0)).toBe(0);
// 16 тайлей; тайль глаза (x3, y14 → id 5): нижний ряд арта — волос+глаз
const resolved = resolveTiles(m.model);
expect(Object.keys(resolved).length).toBe(16);
expect(resolved[5]!.size).toBe(2);
expect(resolved[5]!.faces[4]).toEqual(['#6e4a30', '#94949e', '#6e4a30', '#2a2624']);
// перекраска палитрой красит и текстуры
const tinted = mannequin({ skin: '#c9a882', eyes: '#402a1e' });
const rt = resolveTiles(tinted.model);
expect(rt[5]!.faces[4]).toEqual(['#6e4a30', '#c9a882', '#6e4a30', '#402a1e']);
});
it('палитра — параметр: слоты красятся, форма не меняется', () => {
const plain = mannequin().model;
const tinted = mannequin({ torso: '#111111', skin: '#222222' }).model;
expect(JSON.stringify(mannequin().model)).toBe(JSON.stringify(plain)); // дефолт детерминирован
expect(tinted.palette[4]).toBe('#111111'); // торс перекрашен
expect(tinted.palette[7]).toBe('#222222'); // кожа перекрашена
expect(tinted.palette[6]).toBe(plain.palette[6]); // штаны — дефолт
expect(tinted.data).toBe(plain.data); // форма не зависит от палитры
});
it('каждая часть — связное тело (6-связность)', () => {
const { model, rig } = mannequin();
const g = decodeModel(model);
const cells = (bone: number) => {
const out: [number, number, number][] = [];
for (let i = 0; i < g.data.length; i++)
if (g.data[i] !== 0 && rig.binding[i] === bone) {
const x = i % g.sx, y = Math.floor(i / g.sx) % g.sy, z = Math.floor(i / (g.sx * g.sy));
out.push([x, y, z]);
}
return out;
};
for (const bone of [1, 2, 3, 4]) {
const col = cells(bone);
expect(col.length).toBeGreaterThan(3);
// BFS от первой клетки: все воксели части достижимы соседством граней
const seen = new Set([col[0]!.join(',')]);
const queue = [col[0]!];
while (queue.length) {
const [x, y, z] = queue.shift()!;
for (const [nx, ny, nz] of [[x + 1, y, z], [x - 1, y, z], [x, y + 1, z], [x, y - 1, z], [x, y, z + 1], [x, y, z - 1]]) {
const key = `${nx},${ny},${nz}`;
if (seen.has(key)) continue;
if (!col.some((c) => c[0] === nx && c[1] === ny && c[2] === nz)) continue;
seen.add(key);
queue.push([nx, ny, nz]);
}
}
expect(seen.size).toBe(col.length);
}
});
it('поворот ноги двигает ногу, но не торс (жёсткая привязка частей)', () => {
const { model, rig } = mannequin();
const bindPosed = poseVoxels(rig, model, {});
const swing = poseVoxels(rig, model, { legL: { rot: [0.6, 0, 0] } });
for (const a of bindPosed) {
const b = swing.find((v) => v.index === a.index)!;
const moved = Math.abs(a.pos[0] - b.pos[0]) + Math.abs(a.pos[1] - b.pos[1]) + Math.abs(a.pos[2] - b.pos[2]);
const isLegL = rig.binding[a.index] === 1;
if (isLegL) expect(moved).toBeGreaterThan(0.1);
else expect(moved).toBe(0);
}
});
});