build-cell-coarse-city-tileset.mjs 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566
  1. // Reuse verified cell ownership and immutable GLBs. Only rebuild the LOD index.
  2. // Empty root + finite SSE errors avoid whole-city readiness and hard fine-distance gates.
  3. import assert from 'node:assert/strict'
  4. import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs'
  5. import { resolve, join, relative } from 'node:path'
  6. const root = resolve('tools/city-massing/city/cad-bim')
  7. const source = join(root, 'city-progressive-instanced-v1')
  8. const output = join(root, process.argv[2] || 'city-continuous-v8')
  9. assert.ok(!existsSync(output), 'use an immutable generation')
  10. const tree = JSON.parse(readFileSync(join(source, 'tileset.json'), 'utf8'))
  11. const manifest = JSON.parse(readFileSync(join(source, 'manifest.json'), 'utf8'))
  12. assert.equal(manifest.instancedCells, 0, 'index rebuild expects the verified original geometry')
  13. const records = new Map(manifest.records.map(r => [resolve(source, r.uri), r]))
  14. const errors = { far: 128, near: 48 }
  15. const coarseRequestMargin = 14000
  16. const stats = { far: 0, near: 0, fine: 0, contents: 0 }
  17. function adapt(tile) {
  18. const t = structuredClone(tile)
  19. if (tile.content) {
  20. const path = resolve(source, tile.content.uri), record = records.get(path)
  21. assert.ok(record && existsSync(path), path)
  22. t.content.uri = relative(output, path).replaceAll('\\', '/')
  23. stats[record.level]++; stats.contents++
  24. if (record.level === 'far') {
  25. t.geometricError = errors.far
  26. // Keep distant coarse retirement, preloaded before city overview approaches.
  27. const volume = t.boundingVolume.sphere
  28. assert.ok(volume)
  29. t.viewerRequestVolume = { sphere: [...volume.slice(0, 3), volume[3] + coarseRequestMargin] }
  30. } else {
  31. t.geometricError = record.level === 'near' ? errors.near : 0
  32. // Finite SSE is the request gate. A child outside a hard request sphere
  33. // would block traditional REPLACE and is requested anyway by base traversal.
  34. delete t.viewerRequestVolume
  35. }
  36. } else {
  37. t.geometricError = 1000000
  38. delete t.viewerRequestVolume
  39. }
  40. if (t.children?.length) {
  41. t.refine = 'REPLACE'
  42. assert.ok(t === tree.root || t.boundingVolume.sphere || !t.content)
  43. const children = t.content ? t.children.flatMap(child => {
  44. assert.ok(child.content || child.children?.length, 'empty leaf has no fallback')
  45. return child.content ? [child] : child.children
  46. }) : t.children
  47. t.children = children.map(adapt)
  48. }
  49. return t
  50. }
  51. const newRoot = adapt(tree.root)
  52. delete newRoot.content
  53. assert.equal(stats.contents, manifest.records.length)
  54. mkdirSync(output, { recursive: true })
  55. writeFileSync(join(output, 'tileset.json'), JSON.stringify({
  56. ...tree, asset: { version: '1.1', generator: 'Continuous cell replacement with finite SSE' }, root: newRoot,
  57. }, null, 2) + '\n')
  58. writeFileSync(join(output, 'manifest.json'), JSON.stringify({
  59. buildings: manifest.buildings, errors, coarseRequestMargin, ...stats,
  60. source: 'city-progressive-instanced-v1', geometryChanged: false, instancingEnabled: false,
  61. requiredTraversal: 'base', policy: 'Empty root; cell coarse -> bounded local coarse/fine. Finite SSE controls local requests; Parents remain until desired descendants are ready. Distant coarse has a +14000m request margin, bounded by the deployment whole-tree maximum distance. No new GLB, texture or geometry.',
  62. decisions: manifest.decisions.map(d => ({ cell: d.cell, buildingIds: d.buildingIds, wholeCellCoarse: d.wholeCellCoarse })),
  63. records: manifest.records.map(r => ({ ...r, uri: relative(output, resolve(source, r.uri)).replaceAll('\\', '/') })),
  64. runtimeValidated: false,
  65. }, null, 2) + '\n')
  66. console.log(JSON.stringify({ output, buildings: manifest.buildings, errors, ...stats, geometryChanged: false }))