/**
* Тесты креплений предметов к костям риги: покой/поворот кости (предмет
* едет с кистью), сдвиг id тайлей, неизвестная кость, детерминизм.
*/
import { describe, expect, it } from 'vitest';
import { VoxelGrid } from '../../voxel/grid';
import { encodeModel } from '../../models/format';
import type { VoxelModel } from '../../models/format';
import { mannequin } from '../../models/mannequin';
import { backpackModel } from '../../models/generators';
import { attachVoxels, attachTiles } from '../attach';
/** Модель-кубик 1×1×1 одного слота. */
function dot(color: string): VoxelModel {
const g = new VoxelGrid(1, 1, 1);
g.set(0, 0, 0, 1);
return encodeModel(g, { 1: color });
}
const man = mannequin();
const IDLE = { root: { rot: [0, 0, 0] as const } };
describe('attachVoxels', () => {
it('пустой список креплений = число вокселей базы', () => {
const base = attachVoxels(man.rig, man.model, IDLE, []);
expect(base.length).toBe(164); // все воксели манекена
});
it('предмет у точки крепления, цвет — из палитры предмета', () => {
const atts = [{ bone: 'root', model: dot('#ff0000'), pos: [2, 5, -1] as const }];
const v = attachVoxels(man.rig, man.model, IDLE, atts);
const item = v[v.length - 1]!;
expect(item.color).toBe('#ff0000');
expect(item.pos).toEqual([2.5, 5.5, -0.5]); // центр вокселя предмета
expect(v.length).toBe(165);
});
it('предмет на кости руки едет вместе с её поворотом', () => {
const atts = [{ bone: 'armR', model: dot('#00ff00'), pos: [8, 5, 2] as const }];
const idle = attachVoxels(man.rig, man.model, IDLE, atts);
const swing = attachVoxels(man.rig, man.model, { armR: { rot: [0.55, 0, 0] as const } }, atts);
// предмет сместился ровно как воксель кисти (та же аффинна кости)
const armVoxel = (list: typeof idle) => list.find((v) => v.index === 0)!;
const dxItem = idle[idle.length - 1]!.pos[1] - swing[swing.length - 1]!.pos[1];
const dxArm = armVoxel(idle).pos[1] - armVoxel(swing).pos[1];
expect(Math.abs(dxItem - dxArm)).toBeLessThan(1e-9);
expect(Math.abs(dxItem)).toBeGreaterThan(0.5); // рука с предметом реально качнулась
});
it('неизвестная кость — ошибка', () => {
expect(() => attachVoxels(man.rig, man.model, IDLE, [{ bone: 'tail', model: dot('#fff') }]))
.toThrow(/tail/);
});
it('тайли предметов сдвигаются за базу (и в вокселях, и в CloudTiles)', () => {
const g = new VoxelGrid(1, 1, 1);
g.set(0, 0, 0, 1);
const face = btoa(String.fromCharCode(1, 1, 1, 1)); // size 2, слот 1
const item = encodeModel(g, { 1: '#123456' }, {
tiles: { 1: { size: 2, faces: [face, face, face, face, face, face] } }, // тайль id 1
tex: new Uint8Array([1]),
});
const atts = [{ bone: 'root', model: item, pos: [2, 5, -1] as const }];
const v = attachVoxels(man.rig, man.model, IDLE, atts);
// у манекена 19 тайлей (id 1..19) — тайль предмета стал id 20
expect(v[v.length - 1]!.tile).toBe(20);
const tiles = attachTiles(man.model, atts);
expect(Object.keys(tiles).map(Number).sort((a, b) => a - b)).toEqual(
[...Array(20).keys()].map((i) => i + 1),
);
expect(tiles[20]!.faces.length).toBe(6);
});
it('детерминизм: два вызова — глубокое равенство', () => {
const atts = [{ bone: 'root', model: backpackModel(), pos: [2, 5, -1] as const }];
expect(attachVoxels(man.rig, man.model, IDLE, atts))
.toEqual(attachVoxels(man.rig, man.model, IDLE, atts));
});
});