import assert from 'node:assert/strict'; import { existsSync, readFileSync } from 'node:fs'; import { test } from 'node:test'; import vm from 'node:vm'; import ts from 'typescript'; import * as THREE from 'three'; import { RectAreaLightUniformsLib } from 'three/addons/lights/RectAreaLightUniformsLib.js'; function load(relative) { const path=new URL(relative,import.meta.url); const exports={}; const source=readFileSync(path,'utf8'); vm.runInNewContext(ts.transpileModule(source,{compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2022}}).outputText,{exports,require(spec){if(spec==='three')return THREE;if(spec.includes('RectArea'))return {RectAreaLightUniformsLib};return load(new URL(spec+'.ts',path).href);}}); return exports; } function fixture(){ const {MeshGlassRenderer}=load('../src/rendering/MeshGlassRenderer.ts'); const calls=[];let target=null; const renderer={autoClear:true,extensions:{has:()=>false},debug:{},getContext:()=>({isContextLost:()=>false}),getRenderTarget:()=>target,setRenderTarget(t){target=t;calls.push(['target',t]);},setScissorTest(){},clearDepth(){},render(scene,camera){camera.updateMatrixWorld();scene.updateMatrixWorld();calls.push(['render',scene,target]);scene.traverse(o=>{if(o.isMesh)o.onBeforeRender(renderer,scene,camera,o.geometry,o.material,null);});}}; const source=new THREE.Mesh(new THREE.PlaneGeometry(1,1,128,128),new THREE.ShaderMaterial({uniforms:{uTexture:{value:new THREE.Texture()},uDepthMap:{value:new THREE.Texture()},uDepthScale:{value:.25},uDepthBias:{value:0},uIsVideo:{value:false}}})); const b=new MeshGlassRenderer(renderer,source);return {b,renderer,calls,source}; } test('B is a standalone default renderer without A output targets or shaders',()=>{ assert.ok(existsSync(new URL('../src/rendering/MeshGlassRenderer.ts',import.meta.url)),'standalone B module must exist'); const {b,calls}=fixture(); b.sync({width:1,height:1.32,grid:20,curvature:1},.6,new THREE.Vector3(.6,.8,2.6),new THREE.Color()); const camera=new THREE.PerspectiveCamera(),frame=new THREE.Scene();b.render(camera,frame,true); assert.equal('targetA' in b,false);assert.equal('targetB' in b,false); assert.equal(calls.filter(c=>c[0]==='render').length,2);assert.equal(calls.filter(c=>c[0]==='render')[0][2],null); b.dispose(); }); test('B shares replacements and photo transforms without first rendering A',()=>{ const {b,source}=fixture(),shape={width:1,height:1.32,grid:20,curvature:1}; b.sync(shape,.6,new THREE.Vector3(),new THREE.Color());const geometry=b.glass.geometry; source.position.set(.1,-.2,.22);source.scale.set(.8,1.1,1);const texture=new THREE.Texture();source.material.uniforms.uTexture.value=texture; b.sync(shape,.6,new THREE.Vector3(),new THREE.Color());assert.equal(b.glass.geometry,geometry); const shader={uniforms:{},vertexShader:THREE.ShaderLib.physical.vertexShader,fragmentShader:THREE.ShaderLib.physical.fragmentShader};b.material.onBeforeCompile(shader,{}); assert.equal(shader.uniforms.uBPhoto.value,texture);assert.deepEqual(b.scene.children.find(x=>x.geometry===source.geometry).position.toArray(),source.position.toArray()); b.dispose(); }); test('B excludes caller output from owned transmission targets and disposes once',()=>{ const {b,renderer,source}=fixture();b.sync({width:1,height:1,grid:20,curvature:1},.6,new THREE.Vector3(),new THREE.Color()); const output=new THREE.WebGLRenderTarget(20,20);let outputDisposed=0,sourceDisposed=0,glassDisposed=0; output.addEventListener('dispose',()=>outputDisposed++);source.geometry.addEventListener('dispose',()=>sourceDisposed++);b.glass.geometry.addEventListener('dispose',()=>glassDisposed++); b.render(new THREE.PerspectiveCamera(),new THREE.Scene(),true,output);b.dispose();b.dispose(); assert.equal(outputDisposed,0);assert.equal(sourceDisposed,0);assert.equal(glassDisposed,1);output.dispose(); }); test('normal flow selects B, A is an explicit lazy option, and failure never silently selects A',()=>{ const main=readFileSync(new URL('../src/main.ts',import.meta.url),'utf8'),html=readFileSync(new URL('../index.html',import.meta.url),'utf8'); assert.match(html,/value="b" selected/);assert.match(main,/import\('\.\/rendering\/OriginalGlassRenderer'\)/); assert.match(main,/meshRenderer\.render/);assert.match(main,/showMeshFailure/); assert.match(main,/meshRenderer\.restore/);assert.match(main,/originalRenderer\?\.invalidate/); }); test('a compiled B failure stays explicit until retry or context restoration',()=>{ const {b,renderer}=fixture();let first=true;const render=renderer.render.bind(renderer); renderer.render=(...args)=>{if(first){first=false;renderer.debug.onShaderError?.({},null,null,null);}render(...args);}; const camera=new THREE.PerspectiveCamera(),frame=new THREE.Scene(); assert.throws(()=>b.render(camera,frame,true),/shader|failed/i); assert.throws(()=>b.render(camera,frame,true),/shader|failed/i,'Three caches failed programs; absence of a second callback is not success'); b.restore();assert.doesNotThrow(()=>b.render(camera,frame,true));b.dispose(); }); test('fresh Glass Only still releases the hidden transmission target without drawing the photo',()=>{ const {b,renderer,source}=fixture(),internal=new THREE.WebGLRenderTarget(1,1);let frees=0; internal.addEventListener('dispose',()=>frees++); const original=renderer.render.bind(renderer),setTarget=renderer.setRenderTarget.bind(renderer); renderer.render=(scene,camera)=>{ if(scene===b.scene){const output=renderer.getRenderTarget();setTarget(internal);for(const object of scene.children)if(object.visible&&object.geometry&&object!==b.glass){assert.equal(object.material.colorWrite,false);assert.equal(object.material.depthWrite,false);object.onBeforeRender(renderer,scene,camera,object.geometry,object.material,null);}setTarget(output);} original(scene,camera); }; b.render(new THREE.PerspectiveCamera(),new THREE.Scene(),true,null,false);b.dispose(); assert.equal(frees,1);assert.equal(source.material.colorWrite,true);assert.equal(source.material.depthWrite,true,'shared photo material state restores'); }); test('sliding B moves only the actual pane and keeps world-space exits tied to the translated geometry',()=>{ const {b,source}=fixture(),shape={width:1,height:1.32,grid:20,curvature:1}; b.setSlide(-.5);b.sync(shape,.6,new THREE.Vector3(),new THREE.Color()); assert.equal(b.glass.position.x,-.5);assert.equal(source.position.x,0); const geometry=b.glass.geometry; const shader={uniforms:{},fragmentShader:THREE.ShaderLib.physical.fragmentShader};b.material.onBeforeCompile(shader,{}); assert.equal(shader.uniforms.uBPaneCenterX.value,-.5); assert.match(shader.fragmentShader,/side\*uBPaneHalfSize\.x\+uBPaneCenterX-entry\.x/); assert.ok(b.material.clippingPlanes.length===1);assert.equal(b.material.clippingPlanes[0].constant,.55); b.setSlide(-1);b.sync(shape,.6,new THREE.Vector3(),new THREE.Color()); assert.equal(b.glass.geometry,geometry,'opening does not rebuild patterned glass'); assert.equal(shader.uniforms.uBPaneCenterX.value,-1); assert.equal(b.scene.children.find(x=>x.geometry===source.geometry).position.x,0); b.restore();assert.equal(b.glass.position.x,-1);b.dispose(); }); // A CPU depth-buffer oracle around the real production render calls. Geometry // intersections are real Three triangles; this does not execute WebGL shaders. test('B preserves pane depth into the frame pass while foreground and open-door reveal still draw',()=>{ const {b,renderer,source}=fixture(); const {WindowReveal}=load('../src/objects/WindowReveal.ts'); source.position.z=.2;source.scale.set(1.2,1.584,1); const frame=new THREE.Scene(),wood=new THREE.MeshBasicMaterial({side:THREE.DoubleSide}); const reveal=new WindowReveal(wood);reveal.update(1,1.32,1.2,1.584,.6,.2); frame.add(...reveal.meshes);reveal.meshes.forEach(mesh=>mesh.name='reveal'); const front=new THREE.Mesh(new THREE.BoxGeometry(.12,1.6,.16),wood);front.position.set(-.55,0,.66);front.name='foreground';frame.add(front); const originalRender=renderer.render.bind(renderer);let samples=[],depth=[],winner=[]; renderer.clearDepth=()=>{depth.fill(Infinity);}; renderer.render=(scene,camera)=>{ originalRender(scene,camera); if(renderer.autoClear){depth=samples.map(()=>Infinity);winner=samples.map(()=>null);} for(let i=0;ihit.object.visible&&hit.object.material.depthTest!==false); for(const hit of hits){if(hit.object===b.glass&&hit.point.x < -b.material.clippingPlanes[0].constant)continue; if(hit.distance<=depth[i]){if(hit.object.material.colorWrite)winner[i]=hit.object;if(hit.object.material.depthWrite)depth[i]=hit.distance;} } } }; let protectedRear=0,visibleFront=0,visibleOpenReveal=0; for(const opening of [0,.5,1])for(const eye of [[0,0,3],[.6,-.2,1.1],[-.6,-.2,1.1],[0,.5,1.05]]){ b.setSlide(-opening);b.sync({width:1,height:1.32,grid:20,curvature:1},.6,new THREE.Vector3(),new THREE.Color()); const camera=new THREE.PerspectiveCamera();camera.position.set(...eye);camera.lookAt(0,0,.6);camera.updateMatrixWorld(); samples=[];for(const y of [-.6,-.3,0,.3,.6])for(const x of [-.56,-.49,-.4,-.2,0,.2,.4,.49])samples.push(new THREE.Vector3(x,y,.6)); b.render(camera,frame,true); for(let i=0;ihit.object.visible&&!(hit.object===b.glass&&hit.point.x < -b.material.clippingPlanes[0].constant)); const nearest=all[0]?.object??null; assert.equal(winner[i]?.uuid,nearest?.uuid,`opening ${opening}, eye ${eye}, target ${samples[i].toArray()}: rear frame must not overwrite a nearer pane`); if(nearest===b.glass&&all.some(hit=>reveal.meshes.includes(hit.object)))protectedRear++; if(nearest===front)visibleFront++; if(opening>0&&reveal.meshes.includes(nearest))visibleOpenReveal++; } } assert.ok(protectedRear>0,'regression rays actually see rear reveal behind glass'); assert.ok(visibleFront>0,'near-side wood still occludes the pane'); assert.ok(visibleOpenReveal>0,'slid-open areas retain direct reveal coverage'); console.log('B/frame depth oracle:',JSON.stringify({protectedRear,visibleFront,visibleOpenReveal})); b.dispose();reveal.dispose();front.geometry.dispose();wood.dispose();source.geometry.dispose();source.material.dispose(); }); test('B keeps caller render state after a failing frame pass without clearing the pane depth',()=>{ const {b,renderer}=fixture(),frame=new THREE.Scene();let clears=0; renderer.clearDepth=()=>{clears++;};const render=renderer.render.bind(renderer); renderer.render=(scene,camera)=>{if(scene===frame)throw new Error('frame failure');render(scene,camera);}; assert.throws(()=>b.render(new THREE.PerspectiveCamera(),frame,true),/frame failure/); assert.equal(renderer.autoClear,true);assert.equal(clears,0,'frame failure must not discard the current pane depth'); b.dispose(); });