| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102 |
- // Acceptance for the CAD-driven BIM: geometry provenance, scene bounds and export accounting.
- import { readFileSync, existsSync } from 'node:fs'
- import { resolve } from 'node:path'
- import { Cartesian3, Matrix4, Cartographic } from 'cesium'
- import { PRIMARY_AREA_BOUNDS } from './primary-area.mjs'
- const dir = resolve('tools/city-massing/city/cad-bim')
- const seated = JSON.parse(readFileSync(resolve(dir, 'seated-plan.json'), 'utf8'))
- const previewDir = resolve(dir, 'lod-preview')
- const failures = []
- if (!existsSync(resolve(previewDir, 'manifest.json'))) {
- console.log(JSON.stringify({ passed: false, reason: 'no exported manifest yet', expected: resolve(previewDir, 'manifest.json') }, null, 1))
- process.exit(1)
- }
- const manifest = JSON.parse(readFileSync(resolve(previewDir, 'manifest.json'), 'utf8'))
- const tileset = JSON.parse(readFileSync(resolve(previewDir, 'tileset.json'), 'utf8'))
- // 1. Root anchor must be a valid GLB, otherwise Cesium skips the tree.
- const anchorUri = tileset.root?.content?.uri
- if (!anchorUri) failures.push('root has no content anchor')
- else {
- const anchor = readFileSync(resolve(previewDir, anchorUri))
- if (anchor.toString('ascii', 0, 4) !== 'glTF' || anchor.readUInt32LE(4) !== 2 ||
- anchor.readUInt32LE(8) !== anchor.length || anchor.toString('ascii', 16, 20) !== 'JSON')
- failures.push('root anchor GLB header is invalid')
- else {
- try {
- JSON.parse(anchor.toString('utf8', 20, 20 + anchor.readUInt32LE(12)))
- } catch {
- failures.push('root anchor GLB JSON chunk is invalid')
- }
- }
- }
- // 2. Every placed building must trace back to a seated building.
- const byId = new Map(seated.buildings.map((b) => [b.id, b]))
- const placed = new Set(manifest.cells.flatMap((cell) => cell.buildingIds))
- const orphans = [...placed].filter((id) => !byId.has(id))
- if (orphans.length) failures.push(`${orphans.length} placed ids have no seated building (first: ${orphans[0]})`)
- // 3. The primary area as an ENU box; footprints are stored in ENU metres.
- const inverse = Matrix4.inverseTransformation(Matrix4.fromArray(seated.transform), new Matrix4())
- const corners = [
- [PRIMARY_AREA_BOUNDS[0], PRIMARY_AREA_BOUNDS[1]], [PRIMARY_AREA_BOUNDS[2], PRIMARY_AREA_BOUNDS[1]],
- [PRIMARY_AREA_BOUNDS[2], PRIMARY_AREA_BOUNDS[3]], [PRIMARY_AREA_BOUNDS[0], PRIMARY_AREA_BOUNDS[3]],
- ].map(([lon, lat]) => Matrix4.multiplyByPoint(inverse, Cartesian3.fromDegrees(lon, lat), new Cartesian3()))
- const box = {
- minX: Math.min(...corners.map((c) => c.x)), maxX: Math.max(...corners.map((c) => c.x)),
- minY: Math.min(...corners.map((c) => c.y)), maxY: Math.max(...corners.map((c) => c.y)),
- }
- let outside = 0
- for (const building of seated.buildings) {
- for (const [x, y] of building.footprintENU) {
- if (x < box.minX - 0.5 || x > box.maxX + 0.5 || y < box.minY - 0.5 || y > box.maxY + 0.5) {
- outside++
- if (outside <= 3) failures.push(`${building.id} footprint vertex outside the primary area`)
- break
- }
- }
- }
- // 4. Geometry sanity: massing needs a real outline, heights must be usable.
- let degenerateOutlines = 0
- let badHeights = 0
- for (const building of seated.buildings) {
- const ring = building.footprintENU
- if (building.kind === 'massing' && (!Array.isArray(ring) || ring.length < 3)) degenerateOutlines++
- if (!(building.heightM > 0) || !Number.isFinite(building.positionENU?.[2]) || !Number.isFinite(building.foundationBottom))
- badHeights++
- }
- if (degenerateOutlines) failures.push(`${degenerateOutlines} massing buildings have fewer than 3 outline points`)
- if (badHeights) failures.push(`${badHeights} buildings have unusable height or elevation`)
- const byTier = {}
- const byKind = {}
- for (const building of seated.buildings) {
- byTier[building.tier] = (byTier[building.tier] || 0) + 1
- byKind[building.kind] = (byKind[building.kind] || 0) + 1
- }
- const report = {
- passed: failures.length === 0,
- source: seated.source,
- seated: seated.buildings.length,
- byTier,
- byKind,
- construction: seated.construction,
- excludedRelief: (seated.excluded || []).filter((e) => e.relief !== undefined).length,
- exported: {
- placed: placed.size,
- cells: manifest.cellCount,
- coarseCells: manifest.hierarchy?.coarseCellCount ?? 0,
- lod1Bytes: manifest.totals?.lod1?.bytes ?? null,
- lod0Bytes: manifest.totals?.lod0?.bytes ?? null,
- lod1Triangles: manifest.totals?.lod1?.triangles ?? null,
- lod0Triangles: manifest.totals?.lod0?.triangles ?? null,
- },
- outsidePrimaryArea: outside,
- failures: failures.slice(0, 15),
- }
- console.log(JSON.stringify(report, null, 1))
- if (failures.length) process.exitCode = 1
|