Files
sunrise/tests/summit-assets.test.ts
T

85 lines
4.9 KiB
TypeScript

import { test } from 'node:test';
import assert from 'node:assert/strict';
import { BufferGeometry, Float32BufferAttribute } from 'three';
import { encodeSummitGeometry, decodeSummitGeometry, loadSummitAssets } from '../src/scene/summitAssets.ts';
import { gzipSync } from 'node:zlib';
import { buildSummitGeometry,stitchSummitGeometry } from '../src/scene/summitGeometry.ts';
import { createSummitWalking } from '../src/controls/summitWalking.ts';
import { createTerrainGeometry,terrainGeometryHeightAt } from '../src/scene/terrainGeometry.ts';
import { applySummitTerrain } from '../src/scene/summitTerrain.ts';
import { readFileSync } from 'node:fs';
import { SUMMIT_ASSETS } from '../src/constants/summitAssets.ts';
import { SUMMIT_MATERIALS } from '../src/constants/summitMaterials.ts';
test('baked summit shares vertices and preserves every triangle attribute exactly',()=>{
const geometry=new BufferGeometry();
const order=[0,1,2,2,1,3];
for(const [name,size] of [['position',3],['normal',3],['uv',2],['color',3],['trailDistance',1],['rockDistance',1]] as const) {
geometry.setAttribute(name,new Float32BufferAttribute(order.flatMap(i=>Array.from({length:size},(_,j)=>i*.123+j*.35)),size));
}
const bytes=encodeSummitGeometry(geometry),restored=decodeSummitGeometry(bytes);
assert.equal(restored.getAttribute('position').count,4);
assert.equal(restored.getIndex()!.count,6);
for(const name of Object.keys(geometry.attributes)) {
const a=geometry.getAttribute(name),b=restored.getAttribute(name);
for(let i=0;i<6;i++) for(let j=0;j<a.itemSize;j++)
assert.equal(b.array[restored.getIndex()!.getX(i)*a.itemSize+j],a.array[i*a.itemSize+j]);
}
assert.throws(()=>decodeSummitGeometry(bytes.slice(0,-4)));
geometry.dispose();restored.dispose();
});
test('summit loader accepts both browser-decoded gzip and raw gzip hosting',async()=>{
const geometry=new BufferGeometry();
for(const [name,size] of [['position',3],['normal',3],['uv',2],['color',3],['trailDistance',1],['rockDistance',1]] as const)
geometry.setAttribute(name,new Float32BufferAttribute(new Float32Array(size*3),size));
const bytes=encodeSummitGeometry(geometry),originalFetch=globalThis.fetch;
try {
for(const automatic of [true,false]) {
globalThis.fetch=async input=>String(input).includes('.json')
? Response.json({version:1,bufferHash:'test',byteLength:bytes.byteLength})
: new Response(automatic?bytes:gzipSync(new Uint8Array(bytes)),{headers:automatic?{'Content-Encoding':'gzip'}:{}});
const loaded=await loadSummitAssets();assert.equal(loaded.getIndex()!.count,3);loaded.dispose();
}
} finally {globalThis.fetch=originalFetch;geometry.dispose();}
});
test('adaptive interior triangles follow terrain and never shortcut material transition bands',()=>{
const meta=JSON.parse(readFileSync(new URL('../public/terrain/gwanaksan.json',import.meta.url),'utf8'));
const raw=readFileSync(new URL('../public/terrain/gwanaksan.f32',import.meta.url));
const terrain=createTerrainGeometry(meta,new Float32Array(raw.buffer,raw.byteOffset,raw.byteLength/4),true);
applySummitTerrain(terrain,meta);
const walking=createSummitWalking(meta.origin),mesh=buildSummitGeometry(walking,terrain,true);
const p=mesh.getAttribute('position');
let large=0;
for(let i=0;i<p.count;i+=3) {
const x=(p.getX(i)+p.getX(i+1)+p.getX(i+2))/3,z=(p.getZ(i)+p.getZ(i+1)+p.getZ(i+2))/3;
const y=(p.getY(i)+p.getY(i+1)+p.getY(i+2))/3;
assert.ok(Math.abs(y-terrainGeometryHeightAt(terrain,x,z)-.06)<.0001,'surface stays on source triangle');
const edge=Math.max(...[0,1,2].map(j=>Math.hypot(p.getX(i+j)-p.getX(i+(j+1)%3),p.getZ(i+j)-p.getZ(i+(j+1)%3))));
if(edge<=1.4)continue;
large++;
assert.ok(edge<=SUMMIT_ASSETS.interiorMaxEdge+.0001);
const r=SUMMIT_MATERIALS.rockBlend,t=SUMMIT_MATERIALS.edgeBlend;
for(let j=0;j<3;j++) {
const rock=walking.rockDistance(p.getX(i+j),p.getZ(i+j)),path=walking.pathDistance(p.getX(i+j),p.getZ(i+j));
assert.ok(rock<r.innerMeters-r.irregularityMeters || rock>r.outerMeters+r.irregularityMeters &&
(path<t.innerMeters-t.irregularityMeters||path>t.outerMeters+t.irregularityMeters));
}
}
assert.ok(large>1000);terrain.dispose();mesh.dispose();
});
test('coarse triangles reuse adjoining fine-edge vertices and attributes',()=>{
const g=new BufferGeometry();
const points=[[0,0,0],[4,0,0],[0,0,4],[0,0,0],[2,0,-1],[2,0,0],[2,0,0],[2,0,-1],[4,0,0]];
g.setAttribute('position',new Float32BufferAttribute(points.flat(),3));
g.setAttribute('color',new Float32BufferAttribute(points.flatMap(p=>p[0]===2&&p[2]===0?[.2,.3,.4]:[1,1,1]),3));
const stitched=stitchSummitGeometry(g),p=stitched.getAttribute('position'),color=stitched.getAttribute('color');
assert.equal(p.count,12);
let midpointUses=0;
for(let i=0;i<p.count;i++)if(p.getX(i)===2&&p.getZ(i)===0){midpointUses++;assert.equal(color.getX(i),Math.fround(.2));}
assert.equal(midpointUses,4);
g.dispose();stitched.dispose();
});