verify-lod-district.mjs 2.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657
  1. // Static LOD gate: validates spatial cells, REPLACE hierarchy, nonempty content and LOD byte/triangle reduction.
  2. import { readFileSync, statSync } from 'node:fs'
  3. import { resolve, dirname, join } from 'node:path'
  4. const dir = resolve(process.argv[2] || 'tools/city-massing/district/lod-preview'),
  5. tile = JSON.parse(readFileSync(join(dir, 'tileset.json')), 'utf8'),
  6. manifest = JSON.parse(readFileSync(join(dir, 'manifest.json'), 'utf8'))
  7. const fail = []
  8. const root = tile.root || {}
  9. if (!Array.isArray(root.children) || root.children.length < 2) fail.push('root has fewer than two spatial children')
  10. if (!root.content?.uri) fail.push('root must provide a valid GLB anchor for runtime traversal')
  11. else {
  12. const anchor = readFileSync(join(dir, root.content.uri))
  13. if (anchor.toString('ascii', 0, 4) !== 'glTF' || anchor.readUInt32LE(4) !== 2 ||
  14. anchor.readUInt32LE(8) !== anchor.length || anchor.toString('ascii', 16, 20) !== 'JSON') {
  15. fail.push('root anchor GLB has invalid header')
  16. } else {
  17. try {
  18. JSON.parse(anchor.toString('utf8', 20, 20 + anchor.readUInt32LE(12)))
  19. } catch {
  20. fail.push('root anchor GLB has invalid JSON chunk')
  21. }
  22. }
  23. }
  24. const cellUris = []
  25. for (const cell of root.children || []) {
  26. if (cell.refine !== 'REPLACE') fail.push('cell lacks REPLACE refinement')
  27. if (!cell.content?.uri) fail.push('cell lacks LOD1 content')
  28. else cellUris.push(cell.content.uri)
  29. if (cell.geometricError <= 0) fail.push('LOD1 cell geometricError must be positive')
  30. if (!Array.isArray(cell.children) || cell.children.length < 1) {
  31. fail.push('cell lacks LOD0 children')
  32. continue
  33. }
  34. for (const leaf of cell.children) {
  35. if (leaf.geometricError !== 0) fail.push('LOD0 leaf geometricError must be zero')
  36. if (!leaf.content?.uri) fail.push('LOD0 leaf lacks content')
  37. else cellUris.push(leaf.content.uri)
  38. }
  39. }
  40. for (const uri of cellUris) {
  41. if (!statSync(join(dir, uri), { throwIfNoEntry: false })) fail.push(`missing content ${uri}`)
  42. }
  43. const totals = manifest.totals || {}
  44. if (!(totals.lod1?.bytes < totals.lod0?.bytes)) fail.push('LOD1 bytes are not below LOD0')
  45. if (!(totals.lod1?.triangles < totals.lod0?.triangles)) fail.push('LOD1 triangles are not below LOD0')
  46. if (manifest.count <= 0 || manifest.cellCount < 2) fail.push('manifest is empty')
  47. if (fail.length) throw new Error(fail.join('\n'))
  48. console.log(
  49. JSON.stringify({
  50. passed: true,
  51. scope: manifest.scope,
  52. cells: manifest.cellCount,
  53. count: manifest.count,
  54. low: totals.lod1,
  55. high: totals.lod0,
  56. })
  57. )