import assert from 'node:assert/strict'; import {test} from 'node:test'; import * as THREE from 'three'; import {loadTypescript} from './support/load-typescript.mjs'; const {CameraController}=await loadTypescript(new URL('../src/input/CameraController.ts',import.meta.url)); function pose(aspect,x,y=0){const camera=new THREE.PerspectiveCamera(40,aspect,.1,100),control=new CameraController(camera);control.setCompositionMode('wall');control.setRoomBounds(.76096,.9512,.6);control.setView(x,y);control.update(10,1);camera.updateMatrixWorld(true);return {camera,control};} test('portrait keeps neutral and vertical-only views but widens late horizontal composition',()=>{ for(const y of [-1,0,1]){const p=pose(.46,0,y).camera,l=pose(2.2,0,y).camera;assert.deepEqual(p.position.toArray(),l.position.toArray());assert.deepEqual(p.quaternion.toArray(),l.quaternion.toArray());assert.equal(p.fov,40);} const p=pose(.46,1).camera,l=pose(2.2,1).camera;assert.ok(p.position.z>l.position.z+.1,'portrait retreats');assert.ok(p.fov>40&&p.fov<=65,'bounded wider lens'); }); test('portrait compensation is mirrored, continuous, and preserves anchored scene zoom',()=>{ const a=pose(.46,-1).camera,b=pose(.46,1).camera;assert.ok(Math.abs(a.position.x+b.position.x)<1e-12);assert.equal(a.position.z,b.position.z);assert.equal(a.fov,b.fov); const {camera,control}=pose(.46,0);camera.zoom=2;camera.updateProjectionMatrix();camera.projectionMatrix.elements[8]=.2;camera.projectionMatrix.elements[9]=-.3;camera.projectionMatrixInverse.copy(camera.projectionMatrix).invert(); let previous=camera.position.clone(),fov=40; for(let i=1;i<=100;i++){control.setView(i/100,0);control.update(10,1);assert.ok(camera.position.distanceTo(previous)<.15);assert.ok(camera.fov>=fov-1e-12);assert.equal(camera.zoom,2);assert.equal(camera.projectionMatrix.elements[8],.2);assert.equal(camera.projectionMatrix.elements[9],-.3);const product=new THREE.Matrix4().multiplyMatrices(camera.projectionMatrix,camera.projectionMatrixInverse);if(i>35)assert.ok(product.elements.every((v,j)=>Math.abs(v-(j%5===0?1:0))<1e-10));previous.copy(camera.position);fov=camera.fov;} assert.ok(fov>40);control.reset();control.update(0,1);assert.equal(camera.fov,40);assert.deepEqual(camera.position.toArray(),[0,0,3]); }); test('portrait endpoints show aperture center and unoccluded opposite artwork across photo ratios',async()=>{ const {loadFrameGeometry}=await import('./support/load-frame-geometry.mjs'); const {prepareHouseAsset}=await loadTypescript(new URL('../src/objects/BlenderHouseSurround.ts',import.meta.url)); const {fitWindowScale}=await loadTypescript(new URL('../src/objects/BlenderWindowFrame.ts',import.meta.url)); for(const aspect of [.46,.5,.56,.7])for(const photo of [.2,.56,.8,1,2,5])for(const side of [-1,1]){ const fit=fitWindowScale(aspect,photo)*.82,w=fit*photo*.8,h=fit*.8; const root=loadFrameGeometry(new URL('../public/assets/window/grandma-house-surround.glb',import.meta.url)),house=prepareHouseAsset(root);house.resize(w,h,.6,Math.min(w,h),true); const c=new THREE.PerspectiveCamera(40,aspect,.1,100),ctrl=new CameraController(c);ctrl.setCompositionMode('wall');ctrl.setRoomBounds(w,h,.6);ctrl.setView(side,0);ctrl.update(10,1);c.updateMatrixWorld(true); const door=new THREE.Vector3(0,0,.6).project(c),picture=root.getObjectByName(side>0?'RoomPictureLeft':'RoomPictureRight'); assert.ok(Math.abs(door.x)<.9&&Math.abs(door.y)<.85,'aperture center remains visible'); for(const x of [-.19,0,.19])for(const y of [-.23,0,.23]){ const world=picture.localToWorld(new THREE.Vector3(x,y,.024)),p=world.clone().project(c);assert.ok(Math.abs(p.x)<1&&Math.abs(p.y)<1,JSON.stringify({aspect,photo,side,p:p.toArray()})); } const center=picture.localToWorld(new THREE.Vector3(0,0,.028)),ray=new THREE.Raycaster(c.position,center.clone().sub(c.position).normalize());assert.equal(ray.intersectObject(root,true)[0]?.object,picture,'artwork is not hidden behind the wall return'); house.dispose(); } }); test('portrait rotation compensation is reversible and smooth at both aspect boundaries',()=>{ const {camera,control}=pose(.46,1);const original={p:camera.position.toArray(),q:camera.quaternion.toArray(),fov:camera.fov}; for(const boundary of [.7,1]){camera.aspect=boundary-1e-5;control.update(0,1);const p=camera.position.clone(),q=camera.quaternion.clone(),f=camera.fov;camera.aspect=boundary+1e-5;control.update(0,1);assert.ok(camera.position.distanceTo(p)<.001);assert.ok(camera.quaternion.angleTo(q)<.001);assert.ok(Math.abs(camera.fov-f)<.001);} camera.aspect=2.2;control.update(0,1);assert.equal(camera.fov,40);camera.aspect=.46;control.update(0,1);assert.deepEqual(camera.position.toArray(),original.p);assert.deepEqual(camera.quaternion.toArray(),original.q);assert.equal(camera.fov,original.fov); });