70 lines
4.1 KiB
TypeScript
70 lines
4.1 KiB
TypeScript
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);
|
|
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();
|
|
});
|