import assert from 'node:assert/strict'; import {test} from 'node:test'; import {readFile} from 'node:fs/promises'; import {loadTypescript} from './support/load-typescript.mjs'; test('export shader keeps normal path and can produce transparent photo misses',async()=>{ const source=await readFile(new URL('../src/rendering/MeshRefraction.ts',import.meta.url),'utf8'); assert.match(source,/uBPhotoExport/); assert.match(source,/gl_FragColor\.a\s*=\s*clamp\(bLight\.a/); }); test('pixel extraction flips WebGL rows, crops alpha bounds, and preserves partial alpha',async()=>{ const {prepareExportPixels}=await loadTypescript(new URL('../src/rendering/ExportPixels.ts',import.meta.url)); const bytes=new Uint8Array(4*3*4);bytes.set([30,40,50,128],(1*4+2)*4);bytes.set([10,20,30,255],(2*4+1)*4); const result=prepareExportPixels(bytes,4,3,true); assert.equal(result.width,2);assert.equal(result.height,2); assert.deepEqual([...result.data.slice(0,4)],[10,20,30,255]); assert.deepEqual([...result.data.slice(12,16)],[30,40,50,128]); const full=prepareExportPixels(bytes,4,3,false);assert.equal(full.width,4);assert.equal(full.height,3); assert.throws(()=>prepareExportPixels(new Uint8Array(16),2,2,true),/사진/); }); import * as THREE from 'three'; function captureHarness({readError=false,drawError=false,delay=false}={}){ let target=null,disposed=false,resolveRead;const original={name:'original'}; const renderer={autoClear:false,current:original,viewport:new THREE.Vector4(1,2,3,4),scissor:new THREE.Vector4(2,3,4,5),scissorTest:true,color:new THREE.Color(.2,.3,.4),alpha:.7, getContext:()=>({isContextLost:()=>false}),getDrawingBufferSize:v=>v.set(4,3),getRenderTarget(){return this.current;},getViewport(v){return v.copy(this.viewport);},getScissor(v){return v.copy(this.scissor);},getScissorTest(){return this.scissorTest;},getClearColor(v){return v.copy(this.color);},getClearAlpha(){return this.alpha;}, setRenderTarget(v){this.current=v;},setViewport(v){this.viewport.copy(v);},setScissor(v){this.scissor.copy(v);},setScissorTest(v){this.scissorTest=v;},setClearColor(v,a){this.color.set(v);this.alpha=a;}, async readRenderTargetPixelsAsync(t,x,y,w,h,bytes){assert.equal(this.current,original,'live target restored before readback wait');if(readError)throw Error('read failed');if(delay)await new Promise(r=>resolveRead=r);for(let i=0;i({getContext:()=>({createImageData:(w,h)=>({data:new Uint8ClampedArray(w*h*4)}),putImageData:data=>output=data.data}),toBlob:cb=>cb(new Blob(['png'],{type:'image/png'}))})}; const draw=t=>{target=t;t.addEventListener('dispose',()=>disposed=true);assert.equal(t.texture.colorSpace,THREE.SRGBColorSpace);assert.equal(t.samples,0);renderer.setRenderTarget(t);renderer.setViewport(new THREE.Vector4(0,0,4,3));if(drawError)throw Error('draw failed');}; const restored=()=>{assert.equal(renderer.current,original);assert.deepEqual(renderer.viewport.toArray(),[1,2,3,4]);assert.deepEqual(renderer.scissor.toArray(),[2,3,4,5]);assert.equal(renderer.scissorTest,true);assert.equal(renderer.autoClear,false);assert.equal(renderer.alpha,.7);assert.ok(Math.abs(renderer.color.r-.2)<1e-10);}; return {renderer,root,draw,restored,get output(){return output;},get disposed(){return disposed;},resolve:()=>resolveRead()}; } test('capture restores renderer and disposes target on success and failures without double color encoding',async()=>{ const {captureScenePNG}=await loadTypescript(new URL('../src/rendering/SceneExport.ts',import.meta.url)); for(const settings of [{},{readError:true},{drawError:true}]){ const h=captureHarness(settings),signal=new AbortController().signal; if(settings.readError||settings.drawError)await assert.rejects(captureScenePNG(h.renderer,h.root,'scene',signal,h.draw),/failed/); else {assert.equal((await captureScenePNG(h.renderer,h.root,'photo',signal,h.draw)).type,'image/png');assert.deepEqual([...h.output.slice(0,4)],[128,128,128,255]);} h.restored();assert.equal(h.disposed,true); } }); test('aborting pending readback prevents PNG creation and releases target after readback settles',async()=>{ const {captureScenePNG}=await loadTypescript(new URL('../src/rendering/SceneExport.ts',import.meta.url)); const h=captureHarness({delay:true}),controller=new AbortController(); const promise=captureScenePNG(h.renderer,h.root,'photo',controller.signal,h.draw);h.restored();controller.abort();h.resolve(); await assert.rejects(promise,{name:'AbortError'});assert.equal(h.disposed,true);assert.equal(h.output,undefined); }); test('isolated B draw omits frame and restores background, alpha flags, camera and slide even on failure',async()=>{ const {MeshGlassRenderer}=await loadTypescript(new URL('../src/rendering/MeshGlassRenderer.ts',import.meta.url)); for(const fail of [false,true]){ let current=null;const calls=[]; const renderer={extensions:{has:()=>false},debug:{onShaderError:null},autoClear:true,getContext:()=>({isContextLost:()=>false}),getRenderTarget:()=>current,setRenderTarget:t=>current=t,setScissorTest(){},render(scene,camera){calls.push({scene,camera});scene.children[0]?.onBeforeRender?.();if(fail)throw Error('forced');}}; const material=new THREE.ShaderMaterial({uniforms:{uTexture:{value:new THREE.Texture()},uDepthMap:{value:new THREE.Texture()},uDepthScale:{value:0},uDepthBias:{value:0}},alphaToCoverage:true}); const source=new THREE.Mesh(new THREE.PlaneGeometry(1,1),material),mesh=new MeshGlassRenderer(renderer,source); const bg=new THREE.Color(.2,.3,.4);mesh.scene.background=bg;mesh.setSlide(-.3); const camera=new THREE.PerspectiveCamera();camera.projectionMatrix.elements[8]=.17;const projection=[...camera.projectionMatrix.elements]; const target=new THREE.WebGLRenderTarget(2,2),frame=new THREE.Scene(); const blockerMaterial=new THREE.MeshBasicMaterial();frame.add(new THREE.Mesh(new THREE.PlaneGeometry(1,1),blockerMaterial)); const originalRender=renderer.render;renderer.render=(scene,camera)=>{if(scene===frame)assert.equal(blockerMaterial.colorWrite,false);return originalRender(scene,camera);}; if(fail)assert.throws(()=>mesh.renderForExport(camera,frame,target,true),/forced/);else mesh.renderForExport(camera,frame,target,true); assert.equal(calls.length,fail?1:2);assert.equal(calls[0].camera,camera);assert.equal(calls[0].scene,frame,'frame depth blocks hidden photo without painting object colors'); if(!fail)assert.equal(calls[1].scene,mesh.scene); assert.equal(mesh.scene.background,bg);assert.equal(blockerMaterial.colorWrite,true);assert.equal(renderer.autoClear,true);assert.equal(material.alphaToCoverage,true);assert.equal(mesh.glass.position.x,-.3);assert.deepEqual(camera.projectionMatrix.elements,projection); assert.equal(mesh.internalTargets.has(target),false,'caller-owned export target never becomes an internal target'); assert.equal(mesh.refraction.uniforms.uBPhotoExport.value,false);mesh.dispose();target.dispose();source.geometry.dispose();material.dispose(); } });