| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163 |
- // Verify decoded GLB transforms/feet (not self-reported zBase) against original outline + direct DEM samples.
- import assert from 'node:assert/strict'
- import { readFileSync, writeFileSync } from 'node:fs'
- import { resolve } from 'node:path'
- import { Cartesian3, Cartographic, Matrix4, Quaternion, Math as CM } from 'cesium'
- const out = resolve(process.env.CITY_PREVIEW_DIR || 'tools/city-massing/district/generated')
- const file = readFileSync(resolve(out, 'district.glb'))
- assert.equal(file.toString('ascii', 0, 4), 'glTF')
- assert.equal(file.readUInt32LE(4), 2)
- assert.equal(file.readUInt32LE(8), file.length)
- const jsonLen = file.readUInt32LE(12),
- gltf = JSON.parse(file.toString('utf8', 20, 20 + jsonLen)),
- binStart = 28 + jsonLen
- const plan = JSON.parse(readFileSync('tools/city-massing/district/seated-plan.json'))
- const samples = JSON.parse(readFileSync('tools/city-massing/district/terrain-samples.json'))
- const library = JSON.parse(readFileSync('tools/city-massing/house-library/library.json'))
- const report = JSON.parse(readFileSync(resolve(out, 'placement-report.json')))
- const tile = JSON.parse(readFileSync(resolve(out, 'tileset.json')))
- const root = Matrix4.fromArray(tile.root.transform)
- function nodeMatrices(doc) {
- const map = new Map()
- function visit(i, parent) {
- const n = doc.nodes[i]
- const local = n.matrix
- ? Matrix4.fromArray(n.matrix)
- : Matrix4.fromTranslationQuaternionRotationScale(
- Cartesian3.fromArray(n.translation || [0, 0, 0]),
- new Quaternion(...(n.rotation || [0, 0, 0, 1])),
- Cartesian3.fromArray(n.scale || [1, 1, 1])
- )
- const world = Matrix4.multiply(parent, local, new Matrix4())
- map.set(n.name, { node: n, world })
- for (const c of n.children || []) visit(c, world)
- }
- for (const i of doc.scenes[doc.scene || 0].nodes) visit(i, Matrix4.IDENTITY)
- return map
- }
- function positions(primitive) {
- const a = gltf.accessors[primitive.attributes.POSITION],
- v = gltf.bufferViews[a.bufferView],
- out = []
- assert.equal(a.componentType, 5126)
- assert.equal(a.type, 'VEC3')
- for (let i = 0; i < a.count; i++) {
- const o = binStart + (v.byteOffset || 0) + (a.byteOffset || 0) + i * (v.byteStride || 12)
- out.push(new Cartesian3(file.readFloatLE(o), file.readFloatLE(o + 4), file.readFloatLE(o + 8)))
- }
- return out
- }
- function enu(p) {
- return new Cartesian3(p.x, -p.z, p.y)
- }
- function fromNode(point, m) {
- return enu(Matrix4.multiplyByPoint(m, point, new Cartesian3()))
- }
- function errors(doc) {
- const failures = [],
- mats = nodeMatrices(doc)
- const metrics = []
- for (const b of report.buildings) {
- const actual = mats.get(b.id),
- foot = mats.get(b.id + '_buried_foundation')
- if (!actual || !foot) {
- failures.push(b.id + ' missing node')
- continue
- }
- const src = plan.buildings.find((x) => x.id === b.id)
- const entry = library.archetypes.find((x) => x.id === b.asset)
- const zero = fromNode(new Cartesian3(0, 0, 0), actual.world)
- const world = Matrix4.multiplyByPoint(root, zero, new Cartesian3())
- const ll = Cartographic.fromCartesian(world)
- // Independent absolute ECEF control using WGS84 converter, not degree-to-metre approximations.
- const expected = Cartesian3.fromDegrees(src.center.lon, src.center.lat, src.floorHeight)
- const displacement = Cartesian3.distance(expected, world)
- if (displacement > 0.03) failures.push(b.id + ' centre/height shifted ' + displacement)
- const nominal = entry.nominal_footprint_m,
- w = nominal[0],
- d = nominal[1]
- const corners = [
- [-w / 2, 0, d / 2],
- [w / 2, 0, d / 2],
- [w / 2, 0, -d / 2],
- [-w / 2, 0, -d / 2],
- ].map((c) => fromNode(Cartesian3.fromArray(c), actual.world))
- const cornerError = Math.max(
- ...corners.map((p) => Math.min(...src.footprintENU.map((q) => Math.hypot(p.x - q[0], p.y - q[1]))))
- )
- if (cornerError > 0.05) failures.push(b.id + ' footprint size/orientation mismatch ' + cornerError)
- const points = doc.meshes[actual.node.mesh].primitives.flatMap(positions).map((p) => fromNode(p, actual.world))
- const bottoms = doc.meshes[foot.node.mesh].primitives.flatMap(positions).map((p) => fromNode(p, foot.world))
- const minFoot = Math.min(...bottoms.map((p) => p.z)),
- maxFoot = Math.max(...bottoms.map((p) => p.z)),
- bodyMin = Math.min(...points.map((p) => p.z)),
- roofMax = Math.max(...points.map((p) => p.z))
- // Recompute the original DEM controls instead of trusting seated-plan's groundControls/zBase.
- const inverse = Matrix4.inverseTransformation(root, new Matrix4())
- const controls = src.sampleIndices.map((i) => {
- const coordinate = samples.planPoints[i]
- const height = samples.heights[i]
- assert(Number.isFinite(height), 'Missing original DEM height')
- return Matrix4.multiplyByPoint(inverse, Cartesian3.fromDegrees(coordinate.lon, coordinate.lat, height), new Cartesian3())
- })
- const minGround = Math.min(...controls.map((p) => p.z)),
- maxGround = Math.max(...controls.map((p) => p.z))
- if (minFoot > minGround - 2.8 || maxFoot < maxGround - 0.4)
- failures.push(b.id + ' foundation no longer intersects sampled ground')
- if (bodyMin > maxGround + 0.1 || bodyMin < maxGround - 0.5) failures.push(b.id + ' body floor floated or sank')
- if (roofMax - maxGround < b.actualFloors * 2.95) failures.push(b.id + ' usable facade/roof buried')
- if (points.some((p) => !Number.isFinite(p.x + p.y + p.z))) failures.push(b.id + ' NaN geometry')
- metrics.push({
- id: b.id,
- sourceId: b.sourceId,
- displacement,
- cornerError,
- bodyMin,
- roofMax,
- minGround,
- maxGround,
- foundationMin: minFoot,
- foundationMax: maxFoot,
- visibleFoundation: bodyMin - minGround,
- actualFloors: b.actualFloors,
- })
- }
- return { failures, metrics }
- }
- const baseline = errors(gltf)
- console.log({
- nodes: report.count,
- geoFailures: baseline.failures.slice(0, 5),
- maxCornerError: Math.max(...baseline.metrics.map((x) => x.cornerError)),
- })
- assert.equal(baseline.failures.length, 0, baseline.failures.join('\n'))
- // Negative controls modify actual final GLB scene data, keep all source/metadata untouched.
- const first = report.buildings[0].id
- for (const mode of ['horizontal', 'vertical', 'scale']) {
- let d = structuredClone(gltf)
- let n = d.nodes.find((n) => n.name === first)
- n.translation ||= [0, 0, 0]
- n.scale ||= [1, 1, 1]
- if (mode === 'horizontal') n.translation[0] += 10
- if (mode === 'vertical') n.translation[1] += 5
- if (mode === 'scale') n.scale[0] *= 1.2
- const r = errors(d)
- assert(r.failures.length > 0, 'Mutated ' + mode + ' should fail')
- console.log('negative control detected:', mode)
- }
- writeFileSync(
- resolve(out, 'geometry-verification.json'),
- JSON.stringify(
- {
- passed: true,
- sourceFootprints: plan.buildings.length,
- placed: report.count,
- directTerrainSamples: samples.heights.length,
- metrics: baseline.metrics,
- negativeControls: ['horizontal shift 10m', 'height shift 5m', 'X scale 1.2'],
- },
- null,
- 2
- )
- )
- console.log('PASS actual GLB anchors, dimensions, terrain intersection, facade clearance, and mutation controls')
|