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 {CameraController}=await loadTypescript(new URL('../src/input/CameraController.ts',import.meta.url)); const {prepareHouseAsset}=await loadTypescript(new URL('../src/objects/BlenderHouseSurround.ts',import.meta.url)); const room=()=>loadFrameGeometry(new URL('../public/assets/window/grandma-house-surround.glb',import.meta.url)); test('default room gesture reaches an actual wall-side viewpoint and stays above the floor',()=>{ const c=new THREE.PerspectiveCamera(40,1.4,.1,100),control=new CameraController(c); assert.equal(typeof control.setRoomBounds,'function');control.setRoomBounds(.76096,.9512,.6); for(const x of [-1,1]){control.setView(x,0);control.update(10,1);c.updateMatrixWorld(true);assert.ok(c.position.z>1.25,'corner view leaves enough room to see the wall');assert.ok(Math.abs(new THREE.Vector3(0,0,.6).project(c).x)>.25,'door is shifted to the side');} control.setView(0,-1);control.update(10,1);assert.ok(c.position.y>-.6582304+.1,'eye cannot go under actual floor');assert.ok(Math.atan2(-c.position.y,c.position.z-.6)>Math.PI/8,'lower viewpoint is more oblique than old fixed-z lean'); control.reset();control.update(.016,1);assert.deepEqual(c.position.toArray(),[0,0,3]); }); test('floor continues underneath the eye and the surrounding chamber closes side, ceiling and rear views',()=>{ const root=room(),asset=prepareHouseAsset(root);asset.resize(.76096,.9512,.6);root.updateMatrixWorld(true); const origin=new THREE.Vector3(0,0,3); for(const direction of [[0,-1,0],[1,0,0],[-1,0,0],[0,1,0],[0,0,1]]){ const hits=new THREE.Raycaster(origin,new THREE.Vector3(...direction),.1,20).intersectObject(root,true);assert.ok(hits.length,'room is closed in '+direction); } const floor=new THREE.Raycaster(origin,new THREE.Vector3(0,-1,0),.1,20).intersectObject(root,true)[0];assert.ok(Math.abs(floor.point.y+.6582304)<1e-6);asset.dispose(); }); test('room path preserves pinch anchor, range selection, diagonal continuity and reset',async()=>{ const {SceneZoomController}=await loadTypescript(new URL('../src/input/SceneZoomController.ts',import.meta.url)); const camera=new THREE.PerspectiveCamera(40,.46,.1,100),control=new CameraController(camera),zoom=new SceneZoomController(camera); control.setRoomBounds(.5,.1,.6);zoom.setZoom(2.2,{x:.4,y:-.3});const centers=[camera.projectionMatrix.elements[8],camera.projectionMatrix.elements[9]],offset=zoom.offset.toArray(); control.setRoomBounds(.5,.05,.6);assert.deepEqual([camera.projectionMatrix.elements[8],camera.projectionMatrix.elements[9]],centers);assert.ok(camera.near<.1);assert.ok(new THREE.Matrix4().multiplyMatrices(camera.projectionMatrix,camera.projectionMatrixInverse).equals(new THREE.Matrix4())||new THREE.Matrix4().multiplyMatrices(camera.projectionMatrix,camera.projectionMatrixInverse).elements.every((v,i)=>Math.abs(v-new THREE.Matrix4().elements[i])<1e-12)); const projection=camera.projectionMatrix.toArray(); for(const range of[.35,.65,.75,.9]){control.setMaxOffset(range);control.setView(1,-1);control.update(10,2);const pose=camera.position.clone();control.hold();control.update(.016,2);assert.ok(camera.position.distanceTo(pose)<1e-12);control.reset();control.update(.016,1);assert.deepEqual(camera.position.toArray(),[0,0,3]);} assert.deepEqual(camera.projectionMatrix.toArray(),projection);assert.deepEqual(zoom.offset.toArray(),offset);assert.equal(zoom.value,2.2); control.setRoomBounds(.5,.1,.6,false);assert.equal(camera.near,.1);control.setView(1,-1);control.update(10,1);assert.deepEqual(camera.position.toArray(),[.9,-.9,3]); }); test('room geometry is stable across repeated extreme resizes and preserves all original textures/colors',()=>{ const root=room(),colors=new Map();root.traverse(mesh=>{if(mesh.isMesh)colors.set(mesh.name,[...mesh.geometry.attributes.color.array]);});const asset=prepareHouseAsset(root); const snapshots=()=>{const result=[];root.traverse(mesh=>{if(mesh.isMesh)result.push([mesh.name,[...mesh.geometry.attributes.position.array]]);});return result;}; asset.resize(.76,.95,.6);const original=snapshots(); for(const[w,h]of[[.05,1],[2,.05],[.1,3],[3,.1],[.76,.95]]){asset.resize(w,h,.6);root.traverse(mesh=>{if(!mesh.isMesh)return;for(const name of['position','normal','tangent']){const a=mesh.geometry.attributes[name];if(a)assert.ok(a.array.every(Number.isFinite));}if(colors.has(mesh.name))assert.deepEqual([...mesh.geometry.attributes.color.array],colors.get(mesh.name));});} assert.deepEqual(snapshots(),original);asset.dispose(); }); test('fixed aperture lining seals top and bottom openings when the sash is fully in its pocket',async()=>{ const {prepareFrameAsset,fitWindowScale}=await loadTypescript(new URL('../src/objects/BlenderWindowFrame.ts',import.meta.url)); const {WindowReveal}=await loadTypescript(new URL('../src/objects/WindowReveal.ts',import.meta.url)); const w=fitWindowScale(375/812,5)*.82*.8*5,h=w/5,root=room(),asset=prepareHouseAsset(root),frame=loadFrameGeometry(),fixed=prepareFrameAsset(frame); asset.resize(w,h,.6);fixed.resize(w,h,.6);fixed.setOpen(1); const camera=new THREE.PerspectiveCamera(40,375/812,.1,100),control=new CameraController(camera);control.setRoomBounds(w,h,.6);control.setMaxOffset(.9);control.setView(0,-1);control.update(10,1);camera.updateMatrixWorld(true); const reveal=new WindowReveal(new THREE.MeshBasicMaterial({side:THREE.DoubleSide}));reveal.update(w,h,w*(3-.4704)/2.4,h*(3-.4704)/2.4,.6,.4704); const photo=new THREE.Mesh(new THREE.PlaneGeometry(w*(3-.4704)/2.4,h*(3-.4704)/2.4),new THREE.MeshBasicMaterial());photo.position.z=.4704; const scene=new THREE.Scene();scene.add(root,frame,photo,...reveal.meshes);scene.updateMatrixWorld(true); for(const y of[-.583333333333,.5]){const ray=new THREE.Raycaster();ray.setFromCamera(new THREE.Vector2(0,y),camera);assert.ok(ray.intersectObject(scene,true).length,'uncovered fixed-aperture band at y='+y);} asset.dispose();fixed.dispose();reveal.dispose();photo.geometry.dispose();photo.material.dispose(); }); test('runtime chamber and fixed lining use only the existing plaster atlas tile',()=>{ const root=room(),asset=prepareHouseAsset(root);asset.resize(.76,.95,.6);const uv=root.getObjectByName('HouseRoomShell').geometry.getAttribute('uv'); for(let i=0;i.005&&uv.getX(i)<.495,'U stays inside plaster left column');assert.ok(uv.getY(i)>.505&&uv.getY(i)<.995,'V stays inside plaster upper row');}asset.dispose(); });