import { test } from 'node:test'; import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import { Mesh, MeshBasicMaterial, Raycaster, Vector3 } from 'three'; import { createTerrainGeometry, terrainGeometryHeightAt } from '../src/scene/terrainGeometry.ts'; import { applySummitTerrain } from '../src/scene/summitTerrain.ts'; import { SUMMIT_TERRAIN } from '../src/constants/summitTerrain.ts'; import { SUMMIT_PLATEAU } from '../src/constants/summitPlateau.ts'; import { structurePosition } from '../src/scene/structures.ts'; import { STRUCTURES } from '../src/constants/structures.ts'; const meta = JSON.parse(readFileSync(new URL('../public/terrain/gwanaksan.json', import.meta.url), 'utf8')); const bytes = readFileSync(new URL('../public/terrain/gwanaksan.f32', import.meta.url)); const heights = new Float32Array(bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength)); const start = structurePosition(SUMMIT_TERRAIN.start.latitude, SUMMIT_TERRAIN.start.longitude, meta.origin); test('authored terrain edges join the surrounding slope without vertical steps', () => { const geometry = createTerrainGeometry(meta, heights, true); applySummitTerrain(geometry, meta); const [left, top, right, bottom] = SUMMIT_PLATEAU.atlasBounds; const height = (x: number, z: number) => terrainGeometryHeightAt(geometry, start.x+x, start.z+z); try { // Scan across all four seams, including the last authored half-metre cell. for (const [edge, from, to, horizontal] of [ [left, top, bottom, true], [right, top, bottom, true], [top, left, right, false], [bottom, left, right, false], ] as const) { for (let along = from; along <= to; along += .5) { for (let offset = -1; offset < 1; offset += .25) { const a = horizontal ? height(edge+offset, along) : height(along, edge+offset); const b = horizontal ? height(edge+offset+.25, along) : height(along, edge+offset+.25); assert.ok(Math.abs(b-a) < .5, `terrain seam at ${edge}, ${along}: ${Math.abs(b-a)}m step over 0.25m`); } } } } finally { geometry.dispose(); } }); test('local reconstruction preserves source data and every vertex outside 200m', () => { const geometry = createTerrainGeometry(meta, heights, true); const p = geometry.getAttribute('position'); const before = p.array.slice(); const source = heights.slice(); applySummitTerrain(geometry, meta); let changed = 0; for (let i=0; i= 200) assert.equal(p.getY(i), before[i*3+1]); if (p.getY(i) !== before[i*3+1]) changed++; } assert.ok(changed > 100); assert.deepEqual(heights, source); geometry.dispose(); }); test('radar building and dome clear the saddle from the estimated eye position', () => { const geometry = createTerrainGeometry(meta, heights, true); applySummitTerrain(geometry, meta); const ground = terrainGeometryHeightAt(geometry,start.x,start.z); assert.ok(Math.abs(ground + meta.observerSurfaceElevation - 620) < 1); const radar = structurePosition(STRUCTURES.radar.latitude,STRUCTURES.radar.longitude,meta.origin); radar.y = terrainGeometryHeightAt(geometry,radar.x,radar.z) + 7; const eye = new Vector3(start.x,ground+1.7,start.z); assert.ok(radar.y > eye.y + 8, 'radar must stand above observer on its rock'); const material = new MeshBasicMaterial(); const mesh = new Mesh(geometry,material); const ray = new Raycaster(eye,radar.clone().sub(eye).normalize(),0,eye.distanceTo(radar)); assert.equal(ray.intersectObject(mesh).length,0,'ridge must not hide radar building'); for (const [x,z] of [[start.x,start.z],[radar.x,radar.z],[0,65],[start.x+199,start.z], [start.x-12,start.z-22],[start.x-2,start.z-45],[start.x+5,start.z-35], [start.x-9,start.z-12],[start.x-9,start.z-11],[start.x-9,start.z-13]]) { const hit = new Raycaster(new Vector3(x,1000,z),new Vector3(0,-1,0)).intersectObject(mesh)[0]; assert.ok(Math.abs(hit.point.y-terrainGeometryHeightAt(geometry,x,z))<1e-4); } const joint = terrainGeometryHeightAt(geometry,start.x-9,start.z-12); const shoulders = (terrainGeometryHeightAt(geometry,start.x-9,start.z-11) + terrainGeometryHeightAt(geometry,start.x-9,start.z-13))/2; assert.ok(shoulders-joint>.15,'large fracture must be a physical depression, not only a painted line'); geometry.dispose(); material.dispose(); });