import assert from 'node:assert/strict'; import * as THREE from 'three'; import { WindowReveal } from '../src/objects/WindowReveal.ts'; import { PHOTO_GLASS_DISTANCE } from '../src/constants/glass.ts'; const glassZ = 0.6; const cameraZ = 3; const depthScales = [0, 0.25, 0.4]; const glassGaps = [PHOTO_GLASS_DISTANCE.minGap, PHOTO_GLASS_DISTANCE.maxGap]; const aspects = [0.7, 1.6]; const offsets = [ [0, 0], [0.5, 0], [-0.5, 0], [0, 0.5], [0, -0.5], [0.9, 0], [-0.9, 0], [0, 0.9], [0, -0.9], [0.5, 0.5], [-0.5, 0.5], [0.5, -0.5], [-0.5, -0.5], [0.9, 0.9], [-0.9, 0.9], [0.9, -0.9], [-0.9, -0.9] ]; const samples = [-1, -0.75, -0.5, 0, 0.5, 0.75, 1]; function makePhoto(width, height, z) { const photo = new THREE.Mesh(new THREE.PlaneGeometry(width, height), new THREE.MeshBasicMaterial({ side: THREE.DoubleSide })); photo.position.z = z; photo.updateMatrixWorld(true); return photo; } function makePriorFrame(frontWidth, frontHeight) { const frameThickness = Math.min(frontWidth, frontHeight) * 0.035; const frameDepth = frameThickness * 1.5; const frameZ = glassZ + frameDepth * 0.25; const geometry = new THREE.BoxGeometry(1, 1, 1); const material = new THREE.MeshBasicMaterial({ side: THREE.DoubleSide }); const left = new THREE.Mesh(geometry, material); const right = new THREE.Mesh(geometry, material); const top = new THREE.Mesh(geometry, material); const bottom = new THREE.Mesh(geometry, material); left.position.set(-(frontWidth + frameThickness) * 0.5, 0, frameZ); right.position.set((frontWidth + frameThickness) * 0.5, 0, frameZ); top.position.set(0, (frontHeight + frameThickness) * 0.5, frameZ); bottom.position.set(0, -(frontHeight + frameThickness) * 0.5, frameZ); left.scale.set(frameThickness, frontHeight, frameDepth); right.scale.set(frameThickness, frontHeight, frameDepth); top.scale.set(frontWidth + frameThickness * 2, frameThickness, frameDepth); bottom.scale.set(frontWidth + frameThickness * 2, frameThickness, frameDepth); const meshes = [left, right, top, bottom]; meshes.forEach((mesh) => mesh.updateMatrixWorld(true)); return { meshes, geometry, material }; } function coveredBy(objects, camera, x, y, frontZ) { const point = new THREE.Vector3(x, y, frontZ); const direction = point.clone().sub(camera.position).normalize(); const intersections = new THREE.Raycaster(camera.position, direction).intersectObjects(objects, false); return intersections.length > 0; } let baselineMisses = 0; for (const aspect of aspects) { const baseW = aspect; const baseH = 1; const frontW = baseW * (3 - glassZ) / 3; const frontH = baseH * (3 - glassZ) / 3; for (const depthScale of depthScales) { for (const glassGap of glassGaps) { const backZ = glassZ - glassGap - depthScale; const backW = baseW * (3 - backZ) / 3; const backH = baseH * (3 - backZ) / 3; const photo = makePhoto(backW, backH, backZ); const priorFrame = makePriorFrame(frontW, frontH); const material = new THREE.MeshBasicMaterial({ side: THREE.DoubleSide }); const reveal = new WindowReveal(material); reveal.update(frontW, frontH, backW, backH, glassZ, backZ); reveal.meshes.forEach((mesh) => mesh.updateMatrixWorld(true)); for (const [offsetX, offsetY] of offsets) { const camera = new THREE.PerspectiveCamera(40, 1, 0.1, 100); camera.position.set(offsetX, offsetY, cameraZ); camera.lookAt(0, 0, 0); for (const xRatio of samples) { for (const yRatio of samples) { const x = xRatio * frontW / 2; const y = yRatio * frontH / 2; if (!coveredBy([photo, ...priorFrame.meshes], camera, x, y, glassZ)) baselineMisses += 1; assert.equal(coveredBy([photo, ...reveal.meshes], camera, x, y, glassZ), true, `uncovered aperture ray aspect=${aspect} depth=${depthScale} gap=${glassGap} offset=${offsetX},${offsetY} sample=${xRatio},${yRatio}`); } } } photo.geometry.dispose(); photo.material.dispose(); priorFrame.geometry.dispose(); priorFrame.material.dispose(); reveal.dispose(); material.dispose(); } } } assert.ok(baselineMisses > 0, 'the previous shallow frame must leave some lateral aperture rays uncovered'); const reuseMaterial = new THREE.MeshBasicMaterial({ side: THREE.DoubleSide }); const reusableReveal = new WindowReveal(reuseMaterial); reusableReveal.update(0.8, 0.6, 0.85, 0.64, glassZ, 0.52); const reusableGeometries = reusableReveal.meshes.map((mesh) => mesh.geometry); const reusablePositions = reusableGeometries.map((geometry) => geometry.getAttribute('position')); const disposeCounts = reusableGeometries.map((geometry) => { let count = 0; geometry.addEventListener('dispose', () => { count += 1; }); return () => count; }); reusableReveal.update(0.9, 0.7, 0.96, 0.75, glassZ, 0.48); assert.deepEqual(reusableReveal.meshes.map((mesh) => mesh.geometry), reusableGeometries, 'updates preserve geometry identities'); assert.deepEqual(reusableGeometries.map((geometry) => geometry.getAttribute('position')), reusablePositions, 'updates preserve position buffer identities'); reusableReveal.dispose(); assert.deepEqual(disposeCounts.map((getCount) => getCount()), [1, 1, 1, 1], 'disposal emits exactly once per reveal geometry'); reuseMaterial.dispose(); console.log(`Window reveal coverage passed: ${baselineMisses} previous-frame misses sealed across ${aspects.length} aspects, ${depthScales.length} depth scales, ${glassGaps.length} glass gaps, ${offsets.length} view offsets (including neutral) and ${samples.length ** 2} aperture samples; geometry reuse/disposal verified.`);