import assert from 'node:assert/strict'; import test from 'node:test'; import { readFileSync } from 'node:fs'; import { Mesh, MeshStandardMaterial, Raycaster, Vector3 } from 'three'; import { buildingHeight, createBuildings } from '../src/scene/buildings.ts'; import type { BuildingData } from '../src/scene/buildings.ts'; const data: BuildingData = JSON.parse(readFileSync(new URL('../public/buildings/gwacheon.geojson', import.meta.url), 'utf8')); const metadata = JSON.parse(readFileSync(new URL('../public/terrain/gwanaksan.json', import.meta.url), 'utf8')); const buffer = readFileSync(new URL('../public/terrain/gwanaksan.f32', import.meta.url)); const terrain = { metadata, heights: new Float32Array(buffer.buffer, buffer.byteOffset, buffer.byteLength / 4) }; test('exterior lighting fades through sunrise without emissive window buffers', () => { const city = createBuildings(data, terrain); const materials = city.group.children.map(mesh => (mesh as Mesh).material as MeshStandardMaterial); assert.equal(typeof city.update, 'function'); city.update(-8); assert.ok(materials.every(m => m.emissive.getHex() === 0), 'buildings reflect exterior light instead of emitting'); let geometryBytes = 0, vertices = 0; for (const mesh of city.group.children as Mesh[]) { assert.equal(mesh.geometry.getAttribute('buildingWindow'), undefined); assert.ok(!mesh.geometry.getAttribute('uv'), 'exterior lighting does not use UV coordinates'); assert.ok(mesh.geometry.index, 'shared vertices use an index buffer'); vertices += mesh.geometry.index.count; geometryBytes += mesh.geometry.index.array.byteLength; for (const attribute of Object.values(mesh.geometry.attributes)) geometryBytes += attribute.array.byteLength; } assert.ok(geometryBytes < vertices * 36 * 0.6, 'including indices, geometry uses less than 60% of the previous buffer'); assert.ok(city.spill.intensity.value > 0); const nightSpill = city.spill.intensity.value; city.update(0); assert.ok(city.spill.intensity.value > 0 && city.spill.intensity.value < nightSpill); city.update(6); assert.ok(materials.every(m => m.emissive.getHex() === 0)); assert.equal(city.spill.intensity.value, 0); city.update(-8); assert.equal(city.spill.intensity.value, nightSpill); city.dispose(); }); test('height provenance distinguishes tags, floor estimates, and placeholders', () => { assert.deepEqual(buildingHeight({height:'50 m','building:levels':'10'}), {meters:50,source:'tagged'}); assert.equal(buildingHeight({height:'100 ft'}).meters,30.48); assert.deepEqual(buildingHeight({height:'unknown','building:levels':'20'}),{meters:60,source:'levels'}); assert.deepEqual(buildingHeight({height:'6000',building:'apartments'}),{meters:15,source:'fallback'}); }); test('downloaded footprints are deduplicated, closed, and generate finite merged city geometry', () => { assert.equal(new Set(data.features.map(f=>f.id)).size, data.features.length); for(const f of data.features) for(const ring of f.geometry.coordinates) assert.deepEqual(ring[0],ring.at(-1)); const city=createBuildings(data,terrain); assert.equal(Object.values(city.counts).reduce((a,b)=>a+b,0),data.features.length); assert.ok(city.counts.tagged>0 && city.counts.levels>0 && city.counts.fallback>0); assert.ok(city.group.children.length > 3, 'separate city districts can be culled independently'); assert.ok(city.group.children.length < 100, 'batch buildings instead of one draw call per building'); for(const mesh of city.group.children as Mesh[]) assert.ok(Array.from(mesh.geometry.attributes.position.array).every(Number.isFinite)); city.dispose(); }); test('courtyard holes remain open while roof rays hit the surrounding footprint', () => { const ring=(r:number):[number,number][]=>[[126.991-r,37.429-r],[126.991+r,37.429-r],[126.991+r,37.429+r],[126.991-r,37.429+r],[126.991-r,37.429-r]]; const city=createBuildings({type:'FeatureCollection',features:[{id:'synthetic',properties:{height:'30'},geometry:{type:'Polygon',coordinates:[ring(.0002),ring(.00008)]}}]},terrain); city.group.updateMatrixWorld(true); const center=city.target.clone(); center.y=1000; const ray=new Raycaster(center,new Vector3(0,-1,0)); assert.equal(ray.intersectObject(city.group,true).length,0); ray.ray.origin.x+=12; assert.ok(ray.intersectObject(city.group,true).length>0); city.dispose(); });