// 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 }))