| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667 |
- // One REPLACE hierarchy owns far/near coverage. No global independent-tileset handoff.
- // Reuses immutable bounded source GLBs; no citywide fine geometry generation.
- import assert from 'node:assert/strict'
- import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs'
- import { resolve, join, relative } from 'node:path'
- const root = resolve('tools/city-massing/city/cad-bim')
- const output = join(root, process.argv[2] || 'city-continuous-v5')
- assert.ok(!existsSync(join(output, 'tileset.json')), 'use a new deployment generation')
- const batches = ['pilot', 'batch-urban8000-dedup', 'batch-urban8000b', 'batch-urban8000c',
- 'batch-urban-final', 'batch-urban-tail', 'batch-unfitted-fallback'].map((name) => `lod-${name}-roofed2/streaming-v5`)
- // Keep complete coarse coverage and add the surveyed near-estate detail as a
- // separate spatial branch; the duplicate-ID guard below prevents overlap.
- batches.push('lod-near-estate-pilot-v6/streaming-v14')
- if (process.argv[3]) batches.push(process.argv[3])
- const bounds = { min: [Infinity, Infinity, Infinity], max: [-Infinity, -Infinity, -Infinity] }
- const ids = new Set(), children = [], sources = []
- let transform
- const sphere = (box) => [...box.slice(0, 3), Math.hypot(box[3], box[7], box[11])]
- for (const source of batches) {
- const dir = join(root, source)
- const tree = JSON.parse(readFileSync(join(dir, 'tileset.json'), 'utf8'))
- const manifest = JSON.parse(readFileSync(join(dir, 'manifest.json'), 'utf8'))
- transform ||= tree.root.transform
- assert.deepEqual(tree.root.transform, transform)
- assert.equal(manifest.provenance.closedRoofCaps, true)
- for (const id of manifest.cells.flatMap((c) => c.buildingIds)) {
- assert.ok(!ids.has(id), `duplicate surveyed building ${id}`)
- ids.add(id)
- }
- function adapt(tile) {
- const clone = structuredClone(tile)
- if (tile.content?.uri) clone.content.uri = relative(output, join(dir, tile.content.uri)).replaceAll('\\', '/')
- if (tile.children?.length) {
- // Bounding spheres explicitly disable Cesium's childrenWithinParent box optimization.
- // Otherwise a child outside its narrow request volume can incorrectly cull its coarse parent.
- clone.boundingVolume = { sphere: sphere(tile.boundingVolume.box) }
- clone.geometricError = 1000000 // traversal sentinel, not a measured survey error
- clone.children = tile.children.map(adapt)
- }
- // Keep each coarse content request sphere. Only structural parents and fine GE0
- // tiles may omit it; this lets Cesium retire far coarse partitions by distance while
- // retaining local coarse coverage during fine refinement.
- if (tile.children?.length && !/-lod1\.glb$/.test(tile.content?.uri || '')) delete clone.viewerRequestVolume
- return clone
- }
- children.push(...tree.root.children.map(adapt))
- const box = tree.root.boundingVolume.box
- for (let axis = 0; axis < 3; axis++) {
- bounds.min[axis] = Math.min(bounds.min[axis], box[axis] - box[[3, 7, 11][axis]])
- bounds.max[axis] = Math.max(bounds.max[axis], box[axis] + box[[3, 7, 11][axis]])
- }
- sources.push({ source, count: manifest.count, files: manifest.files.length })
- }
- const center = bounds.min.map((v, i) => (v + bounds.max[i]) / 2)
- const half = bounds.min.map((v, i) => (bounds.max[i] - v) / 2)
- const box = [...center, half[0], 0, 0, 0, half[1], 0, 0, 0, half[2]]
- mkdirSync(output, { recursive: true })
- const anchor = join(root, batches[0], 'root-anchor.glb')
- writeFileSync(join(output, 'root-anchor.glb'), readFileSync(anchor))
- writeFileSync(join(output, 'tileset.json'), JSON.stringify({ asset: { version: '1.1', generator: 'Continuous surveyed city distance-first REPLACE' },
- geometricError: 1000000, root: { transform, boundingVolume: { box }, geometricError: 1000000,
- refine: 'REPLACE', content: { uri: 'root-anchor.glb' }, children } }, null, 2))
- writeFileSync(join(output, 'manifest.json'), JSON.stringify({ buildings: ids.size, sources, boundsENU: bounds,
- policy: 'Continuous bounded LOD1; coarse and fine request spheres are retained for distance-based progressive loading. Fine GE0 uses half-diagonal +250m; coarse uses its source request sphere. Same REPLACE hierarchy; local coarse coverage remains during refinement.',
- closedRoofCaps: true, heightBasedLOD: false }, null, 2))
- console.log(JSON.stringify({ output, buildings: ids.size, sources }))
|