import {test} from 'node:test'; /** CPU geometry/ray contract checks; these do not execute GLSL or verify WebGL pixels. */ import assert from 'node:assert/strict'; import { existsSync } from 'node:fs'; import * as THREE from 'three'; const source = new URL('../src/objects/ExperimentalCourtyard.ts', import.meta.url); assert.ok(existsSync(source), 'the opt-in courtyard module must exist'); const { ExperimentalCourtyard, createCourtyardUniforms, createCourtyardLayout, courtyardShader, courtyardSurfaceShader } = await import(source); assert.equal(typeof createCourtyardUniforms, 'function', 'B needs independent default-disabled uniforms before a courtyard exists'); const defaults = createCourtyardUniforms(); assert.equal(defaults.uYardEnabled.value, 0); assert.notEqual(defaults.uYardBoxMin.value, createCourtyardUniforms().uYardBoxMin.value); // Independent slab traversal matching the shader, compared with actual raster triangles. function intersectBoxes(layout, origin, ray) { let closest = null; for (const box of layout) { let near = -1e20, far = 1e20, nearAxis = -1, farAxis = -1; let missed = false; for (let axis = 0; axis < 3; axis++) { const p = origin.getComponent(axis), d = ray.getComponent(axis); if (Math.abs(d) < 1e-8) { if (p < box.min[axis] || p > box.max[axis]) missed = true; continue; } const a = (box.min[axis] - p) / d, b = (box.max[axis] - p) / d; if (Math.min(a, b) > near) { near = Math.min(a, b); nearAxis = axis; } if (Math.max(a, b) < far) { far = Math.max(a, b); farAxis = axis; } } if (missed || near > far || far < 1e-5) continue; const outside = near >= 1e-5, distance = outside ? near : far; const axis = outside ? nearAxis : farAxis; const normal = new THREE.Vector3().setComponent(axis, Math.sign(ray.getComponent(axis)) * (outside ? -1 : 1)); if (!closest || distance < closest.distance) closest = { distance, normal, box }; } return closest; } const yard = new ExperimentalCourtyard(); assert.equal(yard.group.visible, false, 'ordinary photo mode must not show the courtyard'); assert.equal(yard.uniforms.uYardEnabled.value, 0); assert.equal(yard.group.children.length, 1, 'all courtyard surfaces must share one draw'); const mesh = yard.group.children[0]; assert.ok(mesh.isMesh); assert.equal(mesh.material.uniforms, yard.uniforms, 'raster and B must share live uniform objects'); assert.equal(mesh.material.side, THREE.DoubleSide, 'inside-box hits must match the double-sided raster geometry'); assert.equal(mesh.material.lights, false); assert.ok(mesh.material.fragmentShader.includes(courtyardSurfaceShader)); assert.ok(courtyardShader.includes(courtyardSurfaceShader), 'direct and refracted colors need identical source'); assert.match(courtyardShader, /bool bCourtyardHit\(vec3 origin,\s*vec3 ray,\s*out float distance,\s*out vec3 color\)/); assert.ok(!yard.group.children.some(object => object.isLight)); const uniformIdentities = Object.fromEntries(Object.entries(yard.uniforms).map(([key, uniform]) => [key, uniform])); let seed = 291; const random = () => ((seed = Math.imul(seed, 1664525) + 1013904223 | 0) >>> 0) / 4294967296; let rays = 0, hits = 0, maxError = 0; for (const [width, height] of [[.9, 1.4], [1.8, 1.1], [2.8, .35], [.3, 2.2]]) { const glassZ = .6; yard.resize(width, height, glassZ); yard.setEnabled(true); yard.group.updateMatrixWorld(true); const layout = createCourtyardLayout(width, height, glassZ); assert.ok(layout.length <= 16); assert.equal(mesh.geometry.getAttribute('position').count, layout.length * 36); assert.equal(mesh.geometry.groups.length, 0, 'merged geometry must not preserve per-box draw groups'); const floor = layout.find(box => box.name === 'ground'); assert.ok(Math.abs(floor.max[1] - (-height / 2 - .1 * Math.min(width, height))) < 1e-10); assert.ok(Math.abs(layout.find(box => box.name === 'boundary-wall').max[2] - (glassZ - 3 * height)) < 1e-10); assert.ok(layout.find(box => box.name === 'wood-platform').max[0] < 0, 'the dog foreground on the right stays clear'); for (let boxIndex = 0; boxIndex < layout.length; boxIndex++) { const box = layout[boxIndex]; assert.deepEqual(yard.uniforms.uYardBoxMin.value[boxIndex].toArray(), box.min); assert.deepEqual(yard.uniforms.uYardBoxMax.value[boxIndex].toArray(), box.max); assert.equal(yard.uniforms.uYardBoxKind.value[boxIndex], box.kind); assert.ok(box.max[2] < glassZ, `${box.name} must be outside the glass`); assert.ok(box.min.every((min, axis) => min < box.max[axis])); } const cases = []; // Exit rays from glass, side/under views, both signs and exactly parallel slabs. for (let i = 0; i < 1600; i++) { cases.push([ new THREE.Vector3((random() - .5) * width * 4, (random() - .5) * height * 3, glassZ + (random() - .7) * height * 5), new THREE.Vector3(random() - .5, random() - .5, random() - .5).normalize() ]); } for (const box of layout) { const center = new THREE.Vector3().fromArray(box.min).add(new THREE.Vector3().fromArray(box.max)).multiplyScalar(.5); for (let axis = 0; axis < 3; axis++) for (const sign of [-1, 1]) { cases.push([center, new THREE.Vector3().setComponent(axis, sign)]); const surface = center.clone().setComponent(axis, sign < 0 ? box.min[axis] : box.max[axis]); cases.push([surface.clone().addScaledVector(new THREE.Vector3().setComponent(axis, sign), height), new THREE.Vector3().setComponent(axis, -sign)]); } } for (const [origin, direction] of cases) { const intersections = new THREE.Raycaster(origin, direction, 1e-5).intersectObject(mesh, false); const expected = intersections[0]; const actual = intersectBoxes(layout, origin, direction); assert.equal(Boolean(actual), Boolean(expected), `hit mismatch at ray ${rays}`); if (expected) { const error = Math.abs(expected.distance - actual.distance); maxError = Math.max(error, maxError); assert.ok(error < 5e-5, `nearest distance mismatch ${error} at ray ${rays}`); // Box junctions have coincident inward/outward faces. Float32 mesh bounds can reverse their tie order. assert.ok(intersections.some(hit => Math.abs(hit.distance - actual.distance) < 5e-5 && actual.normal.dot(hit.face.normal) > .999), `outward normal mismatch at ray ${rays}`); hits++; } rays++; } for (const [key, uniform] of Object.entries(uniformIdentities)) assert.equal(yard.uniforms[key], uniform, `${key} identity must survive resize`); } const previousGeometry = mesh.geometry; let replacedDisposed = 0; previousGeometry.addEventListener('dispose', () => replacedDisposed++); yard.resize(1, 1, .6); assert.equal(replacedDisposed, 1, 'resize must release replaced geometry'); yard.setEnabled(false); assert.equal(yard.group.visible, false); assert.equal(yard.uniforms.uYardEnabled.value, 0); yard.setEnabled(true); assert.equal(yard.group.visible, true); assert.equal(yard.uniforms.uYardEnabled.value, 1); assert.throws(() => yard.resize(0, 1, .6), /positive/); assert.throws(() => yard.resize(1, NaN, .6), /finite/); assert.throws(() => yard.resize(1, 1, Infinity), /finite/); const scene = new THREE.Scene(); scene.add(yard.group); let geometryDisposed = 0, materialDisposed = 0; mesh.geometry.addEventListener('dispose', () => geometryDisposed++); mesh.material.addEventListener('dispose', () => materialDisposed++); yard.dispose(); yard.dispose(); assert.equal(geometryDisposed, 1); assert.equal(materialDisposed, 1); assert.equal(yard.group.parent, null); assert.equal(yard.uniforms.uYardEnabled.value, 0); console.log(JSON.stringify({ test: 'ExperimentalCourtyard CPU geometry/analytic ray contract', rays, hits, maxError, boxes: yard.uniforms.uYardBoxMin.value.length, draws: 1, webglPixels: 'not tested' })); test('optional dog contact shadow uses the shared yard shader without another mesh or pass',()=>{ const yard=new ExperimentalCourtyard();yard.setDogContact(.2,.35,.12,.6); assert.deepEqual(yard.uniforms.uYardDogContact.value.toArray(),[.2,.35,.12,.6]); assert.equal(yard.group.children.length,1);assert.match(yard.group.children[0].material.fragmentShader,/uYardDogContact/);assert.match(courtyardShader,/uYardDogContact/);yard.dispose(); });