import assert from 'node:assert/strict'; import {test} from 'node:test'; import * as THREE from 'three'; import {loadTypescript} from './support/load-typescript.mjs'; import {loadFrameGeometry} from './support/load-frame-geometry.mjs'; const {prepareHouseAsset}=await loadTypescript(new URL('../src/objects/BlenderHouseSurround.ts',import.meta.url)); const {createPatternedGlassGeometry}=await loadTypescript(new URL('../src/rendering/PatternedGlassGeometry.ts',import.meta.url)); const room=()=>loadFrameGeometry(new URL('../public/assets/window/grandma-house-surround.glb',import.meta.url)); // Regression: the lining's BACK face must clear even the highest translated // pane vertex, not merely put its front face in front of the pane's base plane. test('fixed pocket lining clears the actual bumpy pane envelope across shapes and compositions',()=>{ const root=room(),asset=prepareHouseAsset(root); for(const [width,height] of [[.76,.95],[.15,1.5],[2,.1]])for(const grid of Array.from({length:15},(_,i)=>10+i))for(const curvature of [-1,0,1])for(const wall of [false,true]){ const shape={width,height,grid,curvature},built=createPatternedGlassGeometry(shape); built.geometry.computeBoundingBox();const front=built.geometry.boundingBox.max.z; asset.resize(width,height,.6,Math.min(width,height),wall,shape); const p=root.getObjectByName('HouseRoomShell').geometry.attributes.position; // Four chamber boxes precede the two pocket-side lining boxes (24 vertices each). // The top/bottom strips retain their depth: their Y range never overlaps the pane. let back=Infinity;for(let i=96;i<144;i++)back=Math.min(back,p.getZ(i)); assert.ok(back>front+1e-6,`lining back ${back} must clear pane front ${front}: ${JSON.stringify(shape)}, wall=${wall}`); const s=Math.min(width,height);assert.ok(Math.abs(p.getX(96)+width/2+.002*s)<1e-6,'inner aperture X unchanged'); for(let i=144;iheight/2,'top/bottom lining stays outside pane Y extent');assert.ok(p.getZ(i)>=-1e-8&&p.getZ(i)<=.002*s+1e-8,'top/bottom reveal seal retains its depth');} built.geometry.dispose(); } asset.dispose(); }); test('lining wins pocket rays while leaving the aperture clear through door travel in both room/photo modes',async()=>{ const {fitWindowScale,prepareFrameAsset}=await loadTypescript(new URL('../src/objects/BlenderWindowFrame.ts',import.meta.url)); const {peekingLayout}=await loadTypescript(new URL('../src/objects/PeekingComposition.ts',import.meta.url)); let checked=0; for(const aspect of [.46,2.16])for(const photoMode of ['original','courtyard'])for(const wall of [false,true]){ const fit=fitWindowScale(aspect,.8)*.82,layout=photoMode==='courtyard'?peekingLayout(aspect,.8):{width:fit*.8*.8,height:fit*.8}; const {width:w,height:h}=layout,s=Math.min(w,h),root=room(),asset=prepareHouseAsset(root),frame=loadFrameGeometry(),sash=prepareFrameAsset(frame); const shape={width:w,height:h,grid:10,curvature:1},built=createPatternedGlassGeometry(shape),pane=new THREE.Mesh(built.geometry,new THREE.MeshBasicMaterial());pane.name='pane';pane.position.z=.6; asset.resize(w,h,.6,layout.clearanceScale,wall,shape);sash.resize(w,h,.6); const shell=root.getObjectByName('HouseRoomShell'); for(const open of [0,.5,1]){ sash.setOpen(open);pane.position.x=-w*open;pane.updateMatrixWorld(true);frame.updateMatrixWorld(true); for(const oblique of [-.5,0,.5])for(let i=0;i<48;i++){ const target=new THREE.Vector3(-w/2-.02*s,(-.46+i/47*.92)*h,.6+.005*s); const origin=target.clone().add(new THREE.Vector3(oblique*s,0,s)); const ray=new THREE.Raycaster(origin,target.clone().sub(origin).normalize(),0,2*s); const lining=ray.intersectObject(shell)[0];assert.ok(lining,'pocket lining remains solid'); const glass=ray.intersectObject(pane).find(hit=>hit.point.x>=-w/2-.05*s); assert.ok(!glass||lining.distance!(h.object.material.clippingPlanes??[]).some(p=>p.distanceToPoint(h.point)<0)); assert.ok(hit,'fixed front wood retains ordinary occlusion'); } pane.geometry.dispose();pane.material.dispose();asset.dispose();sash.dispose(); } console.log('Pocket lining CPU rays:',checked); });