import { describe, expect, it } from 'vitest';
import * as THREE from 'three';
import {
createQuadLayer, createSpriteLayer, makeParticleLightUniforms,
type ParticleLayer, type ParticlePoint,
} from '../particles';
const pt = (i: number): ParticlePoint => ({ pos: [i, 1, 2], color: '#a8bacb' });
const lights = makeParticleLightUniforms();
/** Регрессия #218: держатель слоя (WeatherSystem) держит методы, снятые со
* слоя в момент создания (и вызывает их каждый тик). Раньше рост ёмкости
* перезаписывал методы слоя методами свежей реализации — но снятая ранее
* ссылка продолжала указывать в мёртвое замыкание: каждый тик сверх
* ёмкости создавал новый слой с однократной записью, и замороженные
* снимки дождя копились в сцене (пелена на экране). Теперь фасад
* стабильн: сколько бы ни было ростов, в сцене ровно один меш. */
describe('слои частиц: рост ёмкости не плодит меши (#218)', () => {
const factories: [string, (scene: THREE.Scene, cap: number) => ParticleLayer][] = [
['спрайты', (s, cap) => createSpriteLayer(s, cap, 3, lights, false)],
['квады', (s, cap) => createQuadLayer(s, cap, lights, false)],
];
for (const [name, make] of factories) {
it(`${name}: превышение ёмкости подменяет меш, а не плодит слои`, () => {
const scene = new THREE.Scene();
const layer = make(scene, 2);
const update = layer.update; // как WeatherSystem: метод снят заранее
const meshes = () => scene.children.length;
expect(meshes()).toBe(1);
update([pt(0), pt(1)]);
expect(meshes()).toBe(1);
update([pt(0), pt(1), pt(2), pt(3)]); // рост 2 → 4
expect(meshes()).toBe(1);
update([pt(0), pt(1), pt(2)]); // выше старой ёмкости, ниже новой
expect(meshes()).toBe(1);
update([pt(0), pt(1), pt(2), pt(3)]); // повторное превышение
expect(meshes()).toBe(1);
layer.setTint(0x40465c); // тинт после роста идёт в живой меш
layer.dispose();
expect(meshes()).toBe(0);
layer.dispose(); // повторный dispose безопасен
expect(meshes()).toBe(0);
});
}
});
/** Регрессия ревью: рост ёмкости пересоздаёт impl с дефолтным белым uTint —
* слой должен помнить свой tint и переапплаить его в свежий меш. */
describe('слои частиц: tint переживает рост ёмкости', () => {
const factories: [string, (scene: THREE.Scene, cap: number) => ParticleLayer][] = [
['спрайты', (s, cap) => createSpriteLayer(s, cap, 3, lights, false)],
['квады', (s, cap) => createQuadLayer(s, cap, lights, false)],
];
for (const [name, make] of factories) {
it(`${name}: tint до роста остаётся в новом меше`, () => {
const scene = new THREE.Scene();
const layer = make(scene, 2);
layer.setTint(0x40465c);
layer.update([pt(0), pt(1), pt(2), pt(3)]); // рост 2 → 4
const mesh = scene.children[0] as THREE.Points;
const t = (mesh.material as THREE.ShaderMaterial).uniforms.uTint.value as THREE.Vector3;
expect(t.x).toBeCloseTo(0x40 / 255);
expect(t.y).toBeCloseTo(0x46 / 255);
expect(t.z).toBeCloseTo(0x5c / 255);
layer.dispose();
});
}
});