Files
behind_glass/scripts/test-room-peek.mjs
T
horoli e358e24436 Replace project with current server release and Windows setup
Import the 2026-10-09 source release, Fastify/SQLite backend, restored source
assets, and analytics-enabled server build. Replace files from the previous
release while retaining the existing repository history.

Make builds portable across Windows and Linux, configure local startup,
normalize Windows static asset paths, and fix test fixture cleanup and file
URL handling. Exclude local environment settings, databases, dependencies,
validation output, and TypeScript build cache from version control.

Validation: npm ci, static and server builds, 106 arrival tests, and actual
local HTTP checks passed. The full suite passed 467 of 470 tests; three tests
still require QA fixtures omitted from the supplied release, documented in
README-서버실행.md.
2026-10-09 16:43:35 +09:00

57 lines
6.5 KiB
JavaScript

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<uv.count;i++){assert.ok(uv.getX(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();
});