verify-cell-coarse-city.mjs 4.4 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950
  1. import assert from 'node:assert/strict'
  2. import { readFileSync, writeFileSync, existsSync } from 'node:fs'
  3. import { resolve, join } from 'node:path'
  4. import { parseGlb } from './instance-repeated-meshes.mjs'
  5. import { normalizeConfig } from '../../src/modules/cockpit/services/cockpitConfig.js'
  6. import { planTilesets } from '../../src/modules/cockpit/domain/mapLayers.js'
  7. import { tilesetOptions } from '../../src/modules/cockpit/services/tilesetOptions.js'
  8. const dir = resolve(process.argv[2]),tree=JSON.parse(readFileSync(join(dir,'tileset.json'))),report=JSON.parse(readFileSync(join(dir,'manifest.json')))
  9. assert.equal(tree.root.content,undefined,'empty root must not wait for entire city')
  10. assert.equal(tree.root.viewerRequestVolume,undefined)
  11. const sourceDir=resolve(dir,'../'+report.source),source=JSON.parse(readFileSync(join(sourceDir,'manifest.json')))
  12. const expected=new Map(source.records.map(r=>[resolve(sourceDir,r.uri),r])),seen=new Set(),ids=new Set()
  13. let maxTriangles=0,maxVertices=0,coarseBytes=0,far=0,near=0,fine=0
  14. for(const c of report.decisions)for(const id of c.buildingIds){assert.ok(!ids.has(id));ids.add(id)}
  15. function visit(t){
  16. if(t.content){const path=resolve(dir,t.content.uri),r=expected.get(path);assert.ok(r&&existsSync(path));assert.ok(!seen.has(path));seen.add(path)
  17. const bytes=readFileSync(path),{json}=parseGlb(bytes)
  18. let triangles=0,vertices=0
  19. for(const n of json.nodes)if(n.mesh!==undefined)for(const p of json.meshes[n.mesh].primitives){triangles+=json.accessors[p.indices].count/3;vertices+=json.accessors[p.attributes.POSITION].count}
  20. assert.ok(triangles<=20000&&vertices<=60000,path+' budget');maxTriangles=Math.max(maxTriangles,triangles);maxVertices=Math.max(maxVertices,vertices)
  21. if(r.level==='far'){far++;coarseBytes+=bytes.length;assert.equal(t.geometricError,report.errors.far);assert.deepEqual(t.viewerRequestVolume.sphere,[...t.boundingVolume.sphere.slice(0,3),t.boundingVolume.sphere[3]+report.coarseRequestMargin])}
  22. else{assert.equal(t.viewerRequestVolume,undefined);if(r.level==='near'){near++;assert.equal(t.geometricError,report.errors.near)}else{fine++;assert.equal(t.geometricError,0)}}
  23. }else assert.equal(t.viewerRequestVolume,undefined)
  24. if(t.children?.length){
  25. assert.equal(t.refine,'REPLACE');assert.ok(t===tree.root||t.boundingVolume.sphere)
  26. if(t.content){
  27. assert.ok(t.geometricError>0&&t.geometricError<1000000,'finite content error required')
  28. assert.ok(t.children.every(c=>c.content),'content parent must wait on renderable children, not empty wrappers')
  29. }
  30. }
  31. for(const c of t.children||[])visit(c)
  32. }
  33. visit(tree.root)
  34. assert.equal(ids.size,30172);assert.equal(seen.size,expected.size);assert.equal(far,report.far);assert.equal(near,report.near);assert.equal(fine,report.fine)
  35. const config=normalizeConfig({map:{tilesets:[{url:'/gis/example',progressiveRefinement:true,maximumScreenSpaceError:96}]}})
  36. assert.deepEqual(config.issues,[])
  37. const planned=planTilesets(config.config.map)[0],options=tilesetOptions(planned)
  38. assert.equal(planned.progressiveRefinement,true);assert.equal(options.skipLevelOfDetail,false);assert.equal(options.maximumScreenSpaceError,96)
  39. assert.equal(options.preloadWhenHidden,false);assert.equal(options.preloadFlightDestinations,false)
  40. assert.equal(options.cacheBytes,64*1024*1024);assert.equal(options.maximumCacheOverflowBytes,32*1024*1024)
  41. assert.equal(tilesetOptions(planTilesets(normalizeConfig({map:{tilesets:[{url:'old'}]}}).config.map)[0]).skipLevelOfDetail,true,'old deployments unchanged')
  42. const bad=normalizeConfig({map:{tilesets:[{url:'x',progressiveRefinement:'yes'}]}})
  43. assert.ok(bad.issues.some(s=>s.includes('progressiveRefinement')));assert.equal(planTilesets(bad.config.map)[0].progressiveRefinement,false)
  44. // Nominal SSE examples (actual viewport/FOV/cache may change the distance).
  45. const height=1080,denominator=2*Math.tan(Math.PI/6),sse=options.maximumScreenSpaceError
  46. const result={passed:true,buildings:ids.size,content:seen.size,far,near,fine,maxTriangles,maxVertices,farBytes:coarseBytes,
  47. geometryChanged:false,allContentUrisMatchSource:true,finiteErrorPolicy:report.errors,
  48. nominal1080p60degFineDistanceM:report.errors.near*height/(denominator*sse),coarseRequestMargin:report.coarseRequestMargin,
  49. configPipelineValidated:true,renderingValidated:false,fpsValidated:false}
  50. if(process.argv[3])writeFileSync(process.argv[3],JSON.stringify(result,null,2));console.log(JSON.stringify(result))