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 { SceneZoomController } = await loadTypescript(new URL('../src/input/SceneZoomController.ts', import.meta.url)); const { prepareHouseAsset, houseViewScale } = await loadTypescript(new URL('../src/objects/BlenderHouseSurround.ts', import.meta.url)); const { prepareFrameAsset, fitWindowScale } = await loadTypescript(new URL('../src/objects/BlenderWindowFrame.ts', import.meta.url)); test('gentler .75 default and selectable .90 maximum widen both axes without changing forward distance', () => { const camera = new THREE.PerspectiveCamera(40, 1, .1, 100), controller = new CameraController(camera); for (const x of [-1, 0, 1]) for (const y of [-1, 0, 1]) { controller.setView(x, y); controller.update(10, 1); assert.deepEqual(camera.position.toArray(), [x * .75, y * .75, 3]); const direction = camera.getWorldDirection(new THREE.Vector3()); assert.ok(direction.distanceTo(camera.position.clone().negate().normalize()) < 1e-12); } controller.setMaxOffset(5); controller.setView(-9, 9); controller.update(10, 2); assert.deepEqual(camera.position.toArray(), [-.9, .9, 3], 'sensitivity and excess range cannot exceed the measured envelope'); }); test('old range choices, recenter, pinch hold and anchored projection zoom remain independent', () => { const camera = new THREE.PerspectiveCamera(40, 1, .1, 100), controller = new CameraController(camera), zoom = new SceneZoomController(camera); zoom.setZoom(2.1, { x: .25, y: -.4 }); const projection = camera.projectionMatrix.toArray(), offset = zoom.offset.toArray(); for (const value of [.35, .5, .65, .8, .9]) { controller.setMaxOffset(value); controller.setView(1, -1); controller.update(10, 2); assert.deepEqual(camera.position.toArray(), [value, -value, 3]); controller.reset(); controller.update(.016, 1); assert.deepEqual(camera.position.toArray(), [0, 0, 3]); controller.setView(1, -1); controller.update(10, 2); assert.equal(camera.position.x, value, 'recenter retains the chosen movement range'); } controller.setView(-1, 1); controller.update(1 / 60, 1); controller.hold(); const held = camera.position.toArray(); controller.update(.1, 1); assert.deepEqual(camera.position.toArray(), held); assert.deepEqual(camera.projectionMatrix.toArray(), projection); assert.deepEqual(zoom.offset.toArray(), offset); assert.equal(zoom.value, 2.1); assert.equal(camera.near, .1); assert.equal(camera.fov, 40); }); test('room-relative default and maximum camera paths keep the eye and visible near rays outside the real geometry', () => { const house=loadFrameGeometry(new URL('../public/assets/window/grandma-house-surround.glb',import.meta.url)),frame=loadFrameGeometry(); const roomAsset=prepareHouseAsset(house),frameAsset=prepareFrameAsset(frame),group=new THREE.Group();group.add(house,frame); let rays=0; for(const[vw,vh]of[[375,812],[844,390],[1440,900]])for(const aspect of[.05,.2,.56,.8,1,1.77,5,20]){ const fit=fitWindowScale(vw/vh,aspect)*houseViewScale('wood'),w=fit*aspect*.8,h=fit*.8,s=Math.min(w,h),floor=-h/2-.24*s; roomAsset.resize(w,h,.6);frameAsset.resize(w,h,.6); const camera=new THREE.PerspectiveCamera(40,vw/vh,.1,100),control=new CameraController(camera);control.setRoomBounds(w,h,.6); for(const cap of[.75,.9])for(const open of[0,.5,1]){control.setMaxOffset(cap);frameAsset.setOpen(open);group.updateMatrixWorld(true); for(const x of[-1,-.5,0,.5,1])for(const y of[-1,-.5,0,.5,1]){ control.setView(x,y);control.update(10,1);camera.updateMatrixWorld(true);assert.ok(camera.position.y>floor); for(const px of[-1,0,1])for(const py of[-1,0,1]){ const corner=new THREE.Vector3(px,py,-1).unproject(camera);assert.ok(corner.y>floor-1e-8,'near corners stay above the floor'); const ray=new THREE.Raycaster();ray.setFromCamera(new THREE.Vector2(px,py),camera);const hit=ray.intersectObject(group,true).find(h=>!(h.object.material.clippingPlanes??[]).some(p=>p.distanceToPoint(h.point)<0)); if(hit){const depth=-hit.point.clone().applyMatrix4(camera.matrixWorldInverse).z;assert.ok(depth>=camera.near-1e-7,JSON.stringify({vw,vh,aspect,cap,open,x,y,px,py,depth,near:camera.near,position:camera.position.toArray(),hit:hit.object.name}));}rays++; } } } } console.log('Room envelope visible-ray and floor checks:',rays);roomAsset.dispose();frameAsset.dispose(); });