/** Тесты окна частиц перспективной камеры (пирамиды) — без WebGL. */
import { describe, expect, it } from 'vitest';
import { particleWindowPersp } from '../polyWindow';
/** Пирамида «прототипа»: камера в (0, 20, 30), смотрит в target (0, 0, 0)
* сверху-сзади, вертикальный fov 60° (tanHalf 0.577), аспект 16/9. */
function proto(dy0 = -2, dy1 = 40, far = 200) {
const target: [number, number, number] = [0, 0, 0];
const camPos: [number, number, number] = [0, 20, 30];
const f = [0, -0.555, -0.832] as const; // ≈ взгляд в target (нормирован)
const r = [1, 0, 0] as const; // экранная горизонталь
const u = [0, 0.832, -0.555] as const; // экранная вертикаль
return { r, u, f, camPos, target, tanHalf: 0.577, aspect: 16 / 9, near: 0.5, far, dy0, dy1 };
}
const inside = (b: { minX: number; maxX: number; minZ: number; maxZ: number }, x: number, z: number) =>
x >= b.minX && x <= b.maxX && z >= b.minZ && z <= b.maxZ;
describe('particleWindowPersp (окно пирамиды)', () => {
it('точка земли перед камерой внутри окна, за камерой — снаружи', () => {
const b = particleWindowPersp(proto());
expect(inside(b, 0, -10)).toBe(true); // перед камерой, на земле
expect(inside(b, 0, 60)).toBe(false); // позади камеры
expect(inside(b, 0, 35)).toBe(false); // под самой камерой — ниже нижней грани кадра
});
it('окно расширяется от камеры: дальняя земля шире ближней', () => {
const b = particleWindowPersp(proto());
// точка дальше по взгляду на том же луче — всё ещё внутри
expect(inside(b, 30, -40)).toBe(true);
expect(b.maxX).toBeGreaterThan(10); // полуширина растёт с глубиной
});
it('вырожденные углы не дают NaN (окно конечное или пустое)', () => {
const p = proto(-2, 40, 200);
const b = particleWindowPersp({ ...p, tanHalf: 1e-6, far: 1e-6 });
expect(Number.isFinite(b.minX + b.maxX + b.minZ + b.maxZ)).toBe(true);
});
it('фрустум без пересечения со слоем (взгляд в небо) — конечное вырожденное окно', () => {
// far позади near (вызов с cap'ом D=0 при взгляде вверх): полиэдр пуст,
// но контракт — конечный бокс, а не ±Infinity (wrapV делит на размер)
const b = particleWindowPersp(proto(-2, 40, 0.1));
expect(Number.isFinite(b.minX + b.maxX + b.minZ + b.maxZ)).toBe(true);
expect(b.maxX - b.minX).toBeLessThan(1); // вырожденный бокс в точке цели
});
it('far cap режет окно: меньший cap — уже окно', () => {
const wide = particleWindowPersp(proto(-2, 40, 200));
const narrow = particleWindowPersp(proto(-2, 40, 40));
expect(narrow.maxX - narrow.minX).toBeLessThan(wide.maxX - wide.minX);
expect(narrow.maxZ - narrow.minZ).toBeLessThan(wide.maxZ - wide.minZ);
});
});