85 lines
4.9 KiB
TypeScript
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();
|
|
});
|