verify-continuous-city-tileset.mjs 3.3 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162
  1. import assert from 'node:assert/strict'
  2. import { readFileSync, existsSync, writeFileSync } from 'node:fs'
  3. import { resolve, dirname, join } from 'node:path'
  4. import { Cesium3DTileset } from 'cesium'
  5. const dir = resolve(process.argv[2]), tree = JSON.parse(readFileSync(join(dir, 'tileset.json')))
  6. const report = JSON.parse(readFileSync(join(dir, 'manifest.json')))
  7. const sourceRecords = new Map(), ids = new Set()
  8. for (const source of report.sources) {
  9. const sourceDir = resolve(dir, '..', source.source)
  10. const m = JSON.parse(readFileSync(join(sourceDir, 'manifest.json')))
  11. for (const id of m.cells.flatMap((c) => c.buildingIds)) { assert.ok(!ids.has(id)); ids.add(id) }
  12. for (const r of m.files) sourceRecords.set(resolve(sourceDir, r.uri), r)
  13. }
  14. const cesium = new Cesium3DTileset({ skipLevelOfDetail: true })
  15. let content = 0, coarse = 0, fine = 0, maxTriangles = 0, maxVertices = 0
  16. const used = new Set()
  17. function check(tile) {
  18. if (tile.children?.length) {
  19. assert.equal(tile.refine, 'REPLACE')
  20. assert.equal(tile.geometricError, 1000000)
  21. // Coarse parent content keeps its source request sphere so distant coarse
  22. // partitions can retire; structural parents without content remain unbounded.
  23. if (tile.content?.uri && /-lod1\.glb$/.test(tile.content.uri)) {
  24. assert.ok(tile.viewerRequestVolume?.sphere)
  25. } else assert.equal(tile.viewerRequestVolume, undefined)
  26. if (tile !== tree.root) assert.ok(tile.boundingVolume.sphere)
  27. }
  28. if (tile.content?.uri) {
  29. const path = resolve(dir, tile.content.uri)
  30. assert.ok(existsSync(path), path)
  31. assert.ok(!used.has(path), 'same geometry referenced twice')
  32. used.add(path)
  33. if (tile.content.uri !== 'root-anchor.glb') {
  34. const record = sourceRecords.get(path)
  35. assert.ok(record, `unrecorded ${path}`)
  36. content++
  37. maxTriangles = Math.max(maxTriangles, record.triangles)
  38. maxVertices = Math.max(maxVertices, record.vertices)
  39. assert.ok(record.triangles <= 20000 && record.vertices <= 60000)
  40. if (record.level === 'lod1') { coarse++; assert.ok(tile.viewerRequestVolume?.sphere) }
  41. else {
  42. fine++
  43. assert.equal(tile.geometricError, 0)
  44. const sphere = tile.viewerRequestVolume.sphere, box = tile.boundingVolume.box
  45. assert.ok(Math.abs(sphere[3] - Math.hypot(box[3], box[7], box[11]) - 250) < 0.01)
  46. }
  47. }
  48. }
  49. for (const child of tile.children || []) check(child)
  50. }
  51. check(tree.root)
  52. assert.equal(content, sourceRecords.size)
  53. assert.equal(ids.size, report.buildings)
  54. // Version-pinned private Cesium parser checks structure only; no traversal/render frame.
  55. const actual = cesium.loadTileset(new URL(`file:///${join(dir, 'tileset.json').replaceAll('\\', '/')}`).href, tree)
  56. assert.ok(actual.children.length > 0)
  57. cesium.destroy()
  58. const result = { passed: true, buildings: ids.size, content, coarse, fine, maxTriangles, maxVertices,
  59. oneHierarchy: true, cesiumPrivateTreeParsed: true, traversalValidated: false, geometryReused: true,
  60. caveat: 'Offline structural/resource acceptance only; rendering, FPS and GPU memory unmeasured.' }
  61. if (process.argv[3]) writeFileSync(process.argv[3], JSON.stringify(result, null, 2))
  62. console.log(JSON.stringify(result))