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