import * as THREE from 'three' import { PackOpening, packContents } from '../src/packOpening.ts' import { createStudioCubeTexture } from '../src/cardMaterial.ts' export async function verifyPackPreparation() { const startedAt = performance.now() const loader = new THREE.TextureLoader() const paths = ['david-front', 'david-finish-mask', 'timothy-front', 'timothy-finish-mask', 'card-back', 'linen-normal'] const textures = await Promise.all(paths.map((path) => loader.loadAsync(`/reference/${path}.png`))) for (const index of [0, 2, 4]) textures[index].colorSpace = THREE.SRGBColorSpace textures[5].wrapS = textures[5].wrapT = THREE.RepeatWrapping const environment = createStudioCubeTexture() const results = [] try { for (const lightType of ['Point', 'Directional', 'Spot']) { // A fresh context prevents the preceding pixel test from accidentally warming pack shaders. const renderer = new THREE.WebGLRenderer({ antialias: false }) renderer.setSize(192, 264) renderer.outputColorSpace = THREE.SRGBColorSpace renderer.toneMapping = THREE.ACESFilmicToneMapping const shaderErrors = [] renderer.debug.onShaderError = (gl, program, vertex, fragment) => { shaderErrors.push([gl.getProgramInfoLog(program), gl.getShaderInfoLog(vertex), gl.getShaderInfoLog(fragment)].join('\n')) } const scene = new THREE.Scene() scene.background = new THREE.Color('#0b0e14') scene.environment = environment const lightPosition = new THREE.Vector3(2.2, 2.5, 4.4) const light = lightType === 'Point' ? new THREE.PointLight('#fff0d0', 34, 20, 1.7) : lightType === 'Directional' ? new THREE.DirectionalLight('#fff0d0', 5.4) : new THREE.SpotLight('#fff0d0', 54, 20, THREE.MathUtils.degToRad(28), 0.55, 1.7) light.position.copy(lightPosition) scene.add(light) if (light.target) scene.add(light.target) const backMaterial = new THREE.MeshPhysicalMaterial({ map: textures[4], roughness: 0.42, metalness: 0, clearcoat: 0.2, clearcoatRoughness: 0.24, normalMap: null, side: THREE.DoubleSide, }) const pack = new PackOpening({ canvas: renderer.domElement, textures: { David: { artwork: textures[0], mask: textures[1] }, Timothy: { artwork: textures[2], mask: textures[3] }, }, backMaterial, normalMap: textures[5], environment, lightPosition, onChange() {}, }) pack.resize(192, 264) const revealed = [] renderer.domElement.addEventListener('packreveal', (event) => revealed.push(event.detail)) const originalNow = performance.now try { const programsBeforePreparation = renderer.info.programs.length await pack.prepareGPU(renderer, scene) if (pack.root.visible || pack.state !== 'sealed' || revealed.length) { throw new Error('Preparation changed visibility or reveal state') } const preparedPrograms = new Set(renderer.info.programs.map((program) => program.id)) if (!preparedPrograms.size) throw new Error('Preparation compiled no programs') const texturesAfterPreparation = renderer.info.memory.textures const unexpectedPrograms = new Map() const visitedStates = new Set() let renderedFrames = 0 let maximumTextureCount = texturesAfterPreparation const textureGrowth = [] let now = originalNow.call(performance) performance.now = () => now function frame() { pack.tick(now) renderer.render(scene, pack.camera) renderedFrames++ visitedStates.add(pack.state) if (renderer.info.memory.textures > maximumTextureCount) { textureGrowth.push({ state: pack.state, count: renderer.info.memory.textures }) } maximumTextureCount = Math.max(maximumTextureCount, renderer.info.memory.textures) for (const program of renderer.info.programs) { if (!preparedPrograms.has(program.id) && !unexpectedPrograms.has(program.id)) { unexpectedPrograms.set(program.id, { id: program.id, name: program.name, firstState: pack.state }) } } const gl = renderer.getContext() if (shaderErrors.length || gl.isContextLost() || gl.getError() !== gl.NO_ERROR) { throw new Error(`Pack WebGL rendering failed: ${shaderErrors.join('\n')}`) } } scene.add(pack.root) pack.setActive(true) frame() const durations = { opening: 3200, lifting: 620, revealing: 820, advancing: 650 } for (let action = 0; action < 10 && pack.state !== 'complete'; action++) { pack.primary() const duration = durations[pack.state] if (!duration) throw new Error(`Unexpected animated state: ${pack.state}`) const started = now for (const progress of [0, 0.1, 0.25, 0.4, 0.5, 0.65, 0.8, 0.9, 1.001]) { now = started + duration * progress frame() } } if (pack.state !== 'complete' || JSON.stringify(revealed.map(({ fixture, finish, substrate }) => ({ fixture, finish, substrate }))) !== JSON.stringify(packContents)) { throw new Error('First reveal cycle did not complete in authored order') } pack.restart() frame() results.push({ lightType, passed: unexpectedPrograms.size === 0, programsBeforePreparation, programsAfterPreparation: preparedPrograms.size, programsAfterRevealAndRestart: renderer.info.programs.length, unexpectedPrograms: [...unexpectedPrograms.values()], texturesAfterPreparation, maximumTextureCount, textureGrowth, renderedFrames, visitedStates: [...visitedStates], revealed, }) } finally { performance.now = originalNow pack.dispose() backMaterial.dispose() renderer.dispose() renderer.forceContextLoss() } } return { passed: results.every((result) => result.passed), durationMs: Math.round(performance.now() - startedAt), results } } finally { for (const texture of [...textures, environment]) texture.dispose() } }