/** CPU ray/geometry regression, not GLSL or browser pixel execution. */ import assert from 'node:assert/strict'; import * as THREE from 'three'; import {loadTypescript} from './support/load-typescript.mjs'; import {photoSurface,makeDepthTexture,tracePhotoDDA,triangleOracle,snell,exitGlass} from './support/mesh-refraction-reference.mjs'; const {WindowReveal}=await loadTypescript(new URL('../src/objects/WindowReveal.ts',import.meta.url)); const {createPatternedGlassGeometry}=await loadTypescript(new URL('../src/rendering/PatternedGlassGeometry.ts',import.meta.url)); const {attachMeshRefraction}=await loadTypescript(new URL('../src/rendering/MeshRefraction.ts',import.meta.url)); const {fitWindowScale}=await loadTypescript(new URL('../src/objects/BlenderWindowFrame.ts',import.meta.url)); const aspects=[.8,1,1.6],gaps=[.04,.1296,.1576],depths=[0,.25,.4],opens=[0,.5,1],views=[[0,-.9],[0,.9],[-.9,0],[.9,0],[-.9,-.9],[.9,-.9],[-.9,.9],[.9,.9],[0,0]],edges=[[-.995,.01313],[.995,.01313],[.01313,-.995],[.01313,.995],[-.99273,-.995],[-.99273,.995],[.99273,-.995],[.99273,.995]]; const results=[],counts={cases:0,glass:0,open:0,photo:0,reveal:0,photoOnly:0,revealOnly:0,both:0,tir:0,photoFirst:0,revealFirst:0,noGlassAtTarget:0,originalPhotoMissRepaired:0}; function revealShader(uniforms,o,d){let best=null;const v=uniforms.uBRevealVertices.value;for(let wall=0;wall<4;wall++)for(const tri of [[0,1,2],[0,2,3]]){const [a,b,c]=tri.map(k=>v[wall*4+k]);const e1=b.clone().sub(a),e2=c.clone().sub(a),p=d.clone().cross(e2),det=e1.dot(p);if(Math.abs(det)<1e-9)continue;const delta=o.clone().sub(a),u=delta.dot(p)/det,q=delta.clone().cross(e1),w=d.dot(q)/det;if(u<-.00001||w<-.00001||u+w>1.00001)continue;const t=e2.dot(q)/det;if(t<.000001)continue;if(!best||tx===4&&y===4?1:0),matrix=new THREE.Matrix4().compose(new THREE.Vector3(0,0,backZ),new THREE.Quaternion(),new THREE.Vector3(pw,ph,1)),surface=photoSurface(texture,{depthScale:depth,matrix}); const source=new THREE.Mesh(surface.geometry,new THREE.ShaderMaterial({uniforms:{uTexture:{value:texture},uDepthMap:{value:texture},uDepthScale:{value:depth},uDepthBias:{value:0}}}));source.matrixAutoUpdate=false;source.matrix.copy(matrix);source.updateMatrixWorld(true); const reveal=new WindowReveal(new THREE.MeshBasicMaterial({side:THREE.DoubleSide}));reveal.update(fw,fh,pw,ph,z,backZ);const scene=new THREE.Scene();scene.add(...reveal.meshes);scene.updateMatrixWorld(true); const refraction=attachMeshRefraction(new THREE.MeshPhysicalMaterial(),source);refraction.syncReveal(reveal.meshes,scene); const built=createPatternedGlassGeometry({width:fw,height:fh,grid:20,curvature}),glass=new THREE.Mesh(built.geometry,new THREE.MeshBasicMaterial());glass.position.set(centerX,0,z);glass.updateMatrixWorld(true);refraction.sync({width:fw,height:fh,grid:20,curvature},z+built.backZ,new THREE.Color(),centerX); const target=new THREE.Vector3(pw/2*edge[0],ph/2*edge[1],backZ),incident=target.clone().sub(camera).normalize(),entry=new THREE.Raycaster(camera,incident).intersectObject(glass)[0]; let checked,status='direct',direct=check(camera,incident,surface,reveal,refraction.uniforms,`case${i} direct`);assert.notEqual(direct.chosen,'empty',`case${i} direct hole`); if(entry){counts.glass++;const normal=entry.normal.clone().normalize(),internal=snell(incident,normal,1/1.5),local=entry.point.clone().sub(glass.position),exit=exitGlass(local,internal,{width:fw,height:fh,backZ:built.backZ});assert.ok(exit,`case${i} exit`);const outgoing=snell(internal,exit.normal.clone().negate(),1.5);if(!outgoing){counts.tir++;status='tir';}else{checked=check(exit.point.clone().add(glass.position),outgoing,surface,reveal,refraction.uniforms,`case${i} optical`);status=checked.chosen;if(checked.chosen==='empty'){const masked=new THREE.Raycaster(camera,incident).intersectObjects(reveal.meshes)[0];assert.ok(masked,`case${i} unmasked optical hole`);status='masked optical miss';}else counts[checked.chosen]++;if(checked.photoT!==null&&checked.revealT!==null){counts.both++;counts[checked.chosen==='photo'?'photoFirst':'revealFirst']++;}else if(checked.photoT!==null)counts.photoOnly++;else if(checked.revealT!==null){counts.revealOnly++;counts.originalPhotoMissRepaired++;}}} else{counts.open++;checked=direct;counts[checked.chosen]++;} counts.cases++;results.push({i,aspect,gap,depth,open,curvature,camera:[cx,cy],edge,status,...checked}); built.geometry.dispose();glass.material.dispose();reveal.dispose();surface.geometry.dispose();texture.dispose(); } // Targeted ordering counterpart: same boundary ray, but flat foreground photo // sits in front of the wall. Both must hit and the photo must win. { const z=.2204,pw=1.45*.8*(3-z)/3,ph=1.45*(3-z)/3; const t=makeDepthTexture(2,2,()=>1),surface=photoSurface(t,{depthScale:.25,matrix:new THREE.Matrix4().compose(new THREE.Vector3(0,0,z),new THREE.Quaternion(),new THREE.Vector3(pw,ph,1))}); const reveal=new WindowReveal(new THREE.MeshBasicMaterial({side:THREE.DoubleSide}));reveal.update(.928,1.16,pw,ph,.6,z);const scene=new THREE.Scene();scene.add(...reveal.meshes);scene.updateMatrixWorld(true); const source=new THREE.Mesh(surface.geometry,new THREE.ShaderMaterial({uniforms:{uTexture:{value:t}}})),r=attachMeshRefraction(new THREE.MeshPhysicalMaterial(),source);r.syncReveal(reveal.meshes,scene); const hit=check(new THREE.Vector3(.008207625903806859,.49163956178806933,.588056),new THREE.Vector3(.03234743889587834,.452479898306991,-.8911877382599991),surface,reveal,r.uniforms,'targeted photo-first'); assert.equal(hit.chosen,'photo');assert.ok(hit.photoT!==null&&hit.revealT!==null);counts.cases++;counts.photo++;counts.both++;counts.photoFirst++;results.push({i:72,targeted:'photo in front of reveal',...hit}); } assert.ok(counts.originalPhotoMissRepaired>0,'matrix must exercise original bug');assert.ok(counts.open>0&&counts.glass>0);assert.ok(counts.both>0,'matrix must exercise depth ordering'); const report={counts,coverage:{aspects,gaps,depths,opens,views,edges,curvature:[-1,1],stratified:true,cartesian:false},results,limits:'CPU actual geometry and independent triangle oracle only; no WebGL/browser pixel verification. No shader execution. Test sample count is73 (72 stratified + 1 targeted ordering), not exhaustive all combinations.'};console.log(JSON.stringify({counts,coverage:report.coverage},null,2));