// Reuse verified cell ownership and immutable GLBs. Only rebuild the LOD index. // Empty root + finite SSE errors avoid whole-city readiness and hard fine-distance gates. 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 source = join(root, 'city-progressive-instanced-v1') const output = join(root, process.argv[2] || 'city-continuous-v8') assert.ok(!existsSync(output), 'use an immutable generation') const tree = JSON.parse(readFileSync(join(source, 'tileset.json'), 'utf8')) const manifest = JSON.parse(readFileSync(join(source, 'manifest.json'), 'utf8')) assert.equal(manifest.instancedCells, 0, 'index rebuild expects the verified original geometry') const records = new Map(manifest.records.map(r => [resolve(source, r.uri), r])) const errors = { far: 128, near: 48 } const coarseRequestMargin = 14000 const stats = { far: 0, near: 0, fine: 0, contents: 0 } function adapt(tile) { const t = structuredClone(tile) if (tile.content) { const path = resolve(source, tile.content.uri), record = records.get(path) assert.ok(record && existsSync(path), path) t.content.uri = relative(output, path).replaceAll('\\', '/') stats[record.level]++; stats.contents++ if (record.level === 'far') { t.geometricError = errors.far // Keep distant coarse retirement, preloaded before city overview approaches. const volume = t.boundingVolume.sphere assert.ok(volume) t.viewerRequestVolume = { sphere: [...volume.slice(0, 3), volume[3] + coarseRequestMargin] } } else { t.geometricError = record.level === 'near' ? errors.near : 0 // Finite SSE is the request gate. A child outside a hard request sphere // would block traditional REPLACE and is requested anyway by base traversal. delete t.viewerRequestVolume } } else { t.geometricError = 1000000 delete t.viewerRequestVolume } if (t.children?.length) { t.refine = 'REPLACE' assert.ok(t === tree.root || t.boundingVolume.sphere || !t.content) const children = t.content ? t.children.flatMap(child => { assert.ok(child.content || child.children?.length, 'empty leaf has no fallback') return child.content ? [child] : child.children }) : t.children t.children = children.map(adapt) } return t } const newRoot = adapt(tree.root) delete newRoot.content assert.equal(stats.contents, manifest.records.length) mkdirSync(output, { recursive: true }) writeFileSync(join(output, 'tileset.json'), JSON.stringify({ ...tree, asset: { version: '1.1', generator: 'Continuous cell replacement with finite SSE' }, root: newRoot, }, null, 2) + '\n') writeFileSync(join(output, 'manifest.json'), JSON.stringify({ buildings: manifest.buildings, errors, coarseRequestMargin, ...stats, source: 'city-progressive-instanced-v1', geometryChanged: false, instancingEnabled: false, 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.', decisions: manifest.decisions.map(d => ({ cell: d.cell, buildingIds: d.buildingIds, wholeCellCoarse: d.wholeCellCoarse })), records: manifest.records.map(r => ({ ...r, uri: relative(output, resolve(source, r.uri)).replaceAll('\\', '/') })), runtimeValidated: false, }, null, 2) + '\n') console.log(JSON.stringify({ output, buildings: manifest.buildings, errors, ...stats, geometryChanged: false }))