// Build one GIS-aligned far tileset from the coarse LOD1 files of the active CAD batches. // The wrapper intentionally contains no roads or procedural fill; every content GLB comes from // the same seated plan used by the near/fine batches. import assert from 'node:assert/strict' import { readFileSync, writeFileSync, mkdirSync } from 'node:fs' import { join, resolve } from 'node:path' const root = resolve('tools/city-massing/city/cad-bim') const outputName = process.argv[2] || 'far-aligned' const suffix = process.argv[3] || 'aligned' assert.match(outputName, /^far-[a-z0-9-]+$/) assert.ok(['aligned', 'roofed'].includes(suffix), 'unknown CAD batch generation') const out = join(root, outputName) const batches = ['lod-pilot', 'lod-batch-urban8000-dedup', 'lod-batch-urban8000b', 'lod-batch-urban8000c', 'lod-batch-urban-final', 'lod-batch-urban-tail', 'lod-batch-unfitted-fallback'].map((name) => `${name}-${suffix}`) const boxFromBounds = ({ min, max }) => { const center = min.map((value, axis) => (value + max[axis]) / 2) const half = min.map((value, axis) => Math.max(0.001, (max[axis] - value) / 2)) return [center[0], center[1], center[2], half[0], 0, 0, 0, half[1], 0, 0, 0, half[2]] } const boundsFromBox = (box) => ({ min: [box[0] - box[3], box[1] - box[7], box[2] - box[11]], max: [box[0] + box[3], box[1] + box[7], box[2] + box[11]], }) const extend = (target, bounds) => { for (let axis = 0; axis < 3; axis++) { target.min[axis] = Math.min(target.min[axis], bounds.min[axis]) target.max[axis] = Math.max(target.max[axis], bounds.max[axis]) } } mkdirSync(out, { recursive: true }) const children = [] const buildings = new Set() const sourceBatches = [] let transform = null const bounds = { min: [Infinity, Infinity, Infinity], max: [-Infinity, -Infinity, -Infinity] } for (const batch of batches) { const dir = join(root, batch) 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, `${batch}: transform differs from other CAD batches`) if (suffix === 'roofed') assert.equal(manifest.provenance?.closedRoofCaps, true, `${batch}: missing roof caps`) const rootBounds = manifest.boundsENU ? { min: manifest.boundsENU.min, max: manifest.boundsENU.max } : boundsFromBox(tree.root.boundingVolume.box) extend(bounds, rootBounds) for (const cell of manifest.cells) for (const id of cell.buildingIds) { assert.ok(!buildings.has(id), `duplicate building id ${id}`) buildings.add(id) } const coarse = Array.isArray(tree.root.children) ? tree.root.children : [] assert.ok(coarse.length, `${batch}: no coarse children`) for (const tile of coarse) { assert.ok(tile.content?.uri, `${batch}: coarse child has no content`) const childBounds = boundsFromBox(tile.boundingVolume.box) extend(bounds, childBounds) children.push({ id: `${batch}/${tile.content.uri}`, boundingVolume: tile.boundingVolume, geometricError: Math.max(1, tile.geometricError || 16), refine: 'REPLACE', content: { uri: `../${batch}/${tile.content.uri}` }, }) } sourceBatches.push({ batch, buildings: manifest.count, coarseFiles: coarse.length }) } assert.equal(buildings.size, sourceBatches.reduce((sum, item) => sum + item.buildings, 0), 'far wrapper building coverage does not match batch counts') const tileset = { asset: { version: '1.1', generator: 'CAD GIS-aligned far coarse wrapper' }, geometricError: 800, root: { transform, boundingVolume: { box: boxFromBounds(bounds) }, geometricError: 200, refine: 'REPLACE', content: { uri: `../${batches[0]}/root-anchor.glb` }, children, }, } const report = { generator: 'CAD GIS-aligned far coarse wrapper', sourceBatches, buildings: buildings.size, coarseFiles: children.length, closedRoofCaps: suffix === 'roofed', boundsENU: bounds, note: 'Every coarse GLB is emitted from the same seated GIS plan as the fine CAD/BIM batches. No roads or procedural fill are included.', } writeFileSync(join(out, 'tileset.json'), JSON.stringify(tileset, null, 2) + '\n') writeFileSync(join(out, 'manifest.json'), JSON.stringify(report, null, 2) + '\n') console.log(JSON.stringify({ output: out, buildings: buildings.size, coarseFiles: children.length, bounds }))