import { describe, expect, it } from 'vitest';
import { particleWindow, polyAABB } from '../polyWindow';
/** Базис изо-камеры демо (yaw 45°, возвышение как в voxelRenderer) —
* числа совпадают с численной проверкой three.js. */
const ISO = (() => {
const d: [number, number, number] = [Math.cos(Math.PI / 4) * 0.5, 1, Math.sin(Math.PI / 4) * 0.5];
const n = Math.hypot(...d);
const dir = d.map((v) => v / n) as [number, number, number]; // взгляд → камера
const f = [-dir[0]!, -dir[1]!, -dir[2]!] as const; // взгляд камеры
const rx = dir[2]!, rz = -dir[0]!; // r = norm(f × (0,1,0))
const rl = Math.hypot(rx, rz);
const r = [rx / rl, 0, rz / rl] as const;
const u = [
r[1]! * f[2]! - r[2]! * f[1]!, r[2]! * f[0]! - r[0]! * f[2]!, r[0]! * f[1]! - r[1]! * f[0]!,
] as const; // u = r × f
return { r, u, f };
})();
/** Параметры окна как в игре: viewSize 80, кадр 16:9, камера в 4·viewSize. */
const PARAMS = {
r: ISO.r, u: ISO.u, f: ISO.f,
a: 80 * (16 / 9), h: 80,
k0: 0.1 - 320, k1: 800 - 320,
dy0: 0, dy1: 0,
};
/** Точка δ видна ⇔ |r·δ|≤a, |u·δ|≤h, k0 ≤ f·δ ≤ k1 (слою в тесте равна). */
const onScreen = (d: readonly number[]): boolean =>
Math.abs(d[0]! * ISO.r[0]! + d[1]! * ISO.r[1]! + d[2]! * ISO.r[2]!) <= PARAMS.a &&
Math.abs(d[0]! * ISO.u[0]! + d[1]! * ISO.u[1]! + d[2]! * ISO.u[2]!) <= PARAMS.h &&
d[0]! * ISO.f[0]! + d[1]! * ISO.f[1]! + d[2]! * ISO.f[2]! >= PARAMS.k0 &&
d[0]! * ISO.f[0]! + d[1]! * ISO.f[1]! + d[2]! * ISO.f[2]! <= PARAMS.k1;
describe('polyAABB — окно видимости частиц', () => {
it('наземный слой: след кадра симметричен и совпадает с геометрией', () => {
const box = particleWindow(PARAMS);
// след кадра на земле: a·|r.x| + h·|u.x| + вклад глубины вдоль взгляда
const half = PARAMS.a * Math.abs(ISO.r[0]!) + PARAMS.h * Math.abs(ISO.u[0]!) + 40 * Math.abs(ISO.f[0]!);
expect(box.maxX).toBeCloseTo(half, 1);
expect(box.minX).toBeCloseTo(-half, 1);
expect(box.maxZ).toBeCloseTo(half, 1);
expect(box.minZ).toBeCloseTo(-half, 1);
});
it('всё, что видно на экране в слое, лежит в окне (свойство по выборке)', () => {
for (const dy1 of [0, 10, 42]) {
const box = particleWindow({ ...PARAMS, dy0: -2, dy1 });
// псевдослучайная выборка вокруг кадра (детерминированная)
let s = 12345;
const rnd = () => (s = (s * 1103515245 + 12345) % 2147483648) / 2147483648;
for (let i = 0; i < 4000; i++) {
const d = [(rnd() - 0.5) * 700, dy1 * rnd() - 2, (rnd() - 0.5) * 700] as const;
if (d[1]! > dy1) continue;
if (!onScreen(d)) continue;
expect(d[0]!).toBeGreaterThanOrEqual(box.minX - 1e-6);
expect(d[0]!).toBeLessThanOrEqual(box.maxX + 1e-6);
expect(d[2]!).toBeGreaterThanOrEqual(box.minZ - 1e-6);
expect(d[2]!).toBeLessThanOrEqual(box.maxZ + 1e-6);
}
}
});
it('высокий слой сдвигает и расширяет окно: верх экрана видит дальше', () => {
const low = particleWindow(PARAMS);
const high = particleWindow({ ...PARAMS, dy0: -2, dy1: 42 });
expect(high.maxX).toBeGreaterThan(low.maxX);
expect(high.maxZ).toBeGreaterThan(low.maxZ);
// сдвиг от высоты: Δx = |f.x|·(dy1/|f.y|)·... верхняя точка слоя глубже
expect(high.maxX - low.maxX).toBeCloseTo(42 * (ISO.f[0]! / -ISO.f[1]!) * -1, 1);
});
it('полиэдр: окно пустого набора ограничений вырождается в точку', () => {
const box = polyAABB([[[1, 0, 0], 2], [[1, 0, 0], 2], [[0, 1, 0], 3], [[0, 1, 0], 3], [[0, 0, 1], 4], [[0, 0, 1], 4]]);
expect(box.minX).toBeCloseTo(2);
expect(box.maxX).toBeCloseTo(2);
});
});