| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165 |
- // Verify bounded streaming-v2 tiles: request volumes, Draco payloads, budgets, and coverage.
- import assert from 'node:assert/strict'
- import { readFileSync, writeFileSync } from 'node:fs'
- import { resolve, join } from 'node:path'
- import draco3d from 'draco3d'
- const argv = process.argv.slice(2)
- const reportIndex = argv.indexOf('--report')
- const reportPath = reportIndex < 0 ? null : argv.splice(reportIndex, 2)[1]
- const dirs = argv.length ? argv : [process.argv[2]]
- assert.ok(dirs[0], 'batch directory required')
- const decoderModule = await draco3d.createDecoderModule({})
- function parseGlb(path) {
- const bytes = readFileSync(path)
- assert.equal(bytes.toString('ascii', 0, 4), 'glTF', path)
- assert.equal(bytes.readUInt32LE(4), 2, path)
- assert.equal(bytes.readUInt32LE(8), bytes.length, path)
- const jsonLength = bytes.readUInt32LE(12)
- const json = JSON.parse(bytes.toString('utf8', 20, 20 + jsonLength))
- const binOffset = 20 + jsonLength
- assert.equal(bytes.toString('ascii', binOffset + 4, binOffset + 8), 'BIN\0', path)
- const binLength = bytes.readUInt32LE(binOffset)
- return { bytes, json, bin: bytes.subarray(binOffset + 8, binOffset + 8 + binLength) }
- }
- function decodePrimitive(path, json, bin, primitive, translation, bounds) {
- const extension = primitive.extensions?.KHR_draco_mesh_compression
- assert.ok(extension, `${path}: missing Draco extension`)
- const view = json.bufferViews[extension.bufferView]
- assert.ok(view, `${path}: missing Draco bufferView`)
- const encoded = bin.subarray(view.byteOffset || 0, (view.byteOffset || 0) + view.byteLength)
- const decoder = new decoderModule.Decoder()
- const buffer = new decoderModule.DecoderBuffer()
- const mesh = new decoderModule.Mesh()
- let status
- try {
- buffer.Init(new Int8Array(encoded.buffer, encoded.byteOffset, encoded.byteLength), encoded.byteLength)
- status = decoder.DecodeBufferToMesh(buffer, mesh)
- assert.ok(status.ok(), `${path}: ${status.error_msg()}`)
- const points = mesh.num_points()
- const faces = mesh.num_faces()
- assert.equal(points, json.accessors[primitive.attributes.POSITION].count, `${path}: point count mismatch`)
- assert.equal(faces * 3, json.accessors[primitive.indices].count, `${path}: face count mismatch`)
- const attribute = decoder.GetAttributeByUniqueId(mesh, extension.attributes.POSITION)
- assert.equal(attribute.num_components(), 3)
- const positionBytes = points * 3 * 4
- const positionPtr = decoderModule._malloc(positionBytes)
- try {
- decoder.GetAttributeDataArrayForAllPoints(mesh, attribute, decoderModule.DT_FLOAT32, positionBytes, positionPtr)
- const positions = new Float32Array(decoderModule.HEAPF32.buffer, positionPtr, points * 3)
- for (let index = 0; index < points; index++) {
- const enu = [positions[index * 3] + translation[0], -(positions[index * 3 + 2] + translation[2]),
- positions[index * 3 + 1] + translation[1]]
- for (let axis = 0; axis < 3; axis++) {
- assert.ok(Number.isFinite(enu[axis]), `${path}: non-finite position`)
- assert.ok(enu[axis] >= bounds.min[axis] - 0.05 && enu[axis] <= bounds.max[axis] + 0.05,
- `${path}: decoded world position outside tile bounds`)
- }
- }
- } finally { decoderModule._free(positionPtr) }
- const indexBytes = faces * 3 * 4
- const indexPtr = decoderModule._malloc(indexBytes)
- try {
- decoder.GetTrianglesUInt32Array(mesh, indexBytes, indexPtr)
- const indices = new Uint32Array(decoderModule.HEAPU32.buffer, indexPtr, faces * 3)
- for (const index of indices) assert.ok(index < points, `${path}: out-of-range triangle index`)
- } finally { decoderModule._free(indexPtr) }
- return { points, triangles: faces }
- } finally {
- if (status) decoderModule.destroy(status)
- decoderModule.destroy(mesh); decoderModule.destroy(buffer); decoderModule.destroy(decoder)
- }
- }
- function collectTiles(tile, out = []) {
- if (tile.content?.uri) out.push(tile)
- for (const child of tile.children || []) collectTiles(child, out)
- return out
- }
- function boxVolume(box) {
- assert.equal(box.length, 12)
- assert.deepEqual([box[4], box[5], box[6], box[8], box[9], box[10]], [0, 0, 0, 0, 0, 0])
- return Math.max(0.001, box[3] * 2) * Math.max(0.001, box[7] * 2) * Math.max(0.001, box[11] * 2)
- }
- const all = []
- for (const directory of dirs) {
- const dir = resolve(directory)
- const tree = JSON.parse(readFileSync(join(dir, 'tileset.json'), 'utf8'))
- const manifest = JSON.parse(readFileSync(join(dir, 'manifest.json'), 'utf8'))
- assert.equal(manifest.provenance?.gisFootprints, true, `${directory}: GIS provenance missing`)
- assert.equal(manifest.provenance?.roadsProcedural, false, `${directory}: procedural roads provenance`)
- const tiles = collectTiles(tree.root).filter((tile) => tile !== tree.root)
- const records = new Map(manifest.files.map((record) => [record.uri, record]))
- assert.equal(records.size, manifest.files.length, `${directory}: duplicate file records`)
- assert.ok(tiles.length > 0, `${directory}: no content tiles`)
- assert.ok(tree.root.content?.uri, `${directory}: no root anchor`)
- const root = tree.root
- assert.ok(root.children?.length >= 2, `${directory}: too few structural children`)
- const ids = manifest.cells.flatMap((cell) => cell.buildingIds)
- assert.equal(ids.length, manifest.count, `${directory}: wrong building count`)
- assert.equal(new Set(ids).size, manifest.count, `${directory}: duplicate or missing building IDs`)
- const source = JSON.parse(readFileSync(join(dir, '..', 'manifest.json'), 'utf8'))
- assert.deepEqual([...ids].sort(), source.cells.flatMap((cell) => cell.buildingIds).sort(), `${directory}: source coverage changed`)
- const byLevel = { lod1: { triangles: 0, bytes: 0 }, lod0: { triangles: 0, bytes: 0 } }
- let files = 0, triangles = 0, vertices = 0, bytes = 0, maxVertices = 0, maxTriangles = 0
- for (const tile of tiles) {
- assert.ok(tile.viewerRequestVolume?.sphere, `${directory}: content missing viewerRequestVolume`)
- assert.ok(tile.content?.uri !== 'root-anchor.glb', `${directory}: anchor in a request tile`)
- const record = records.get(tile.content.uri)
- assert.ok(record, `${directory}: unrecorded ${tile.content.uri}`)
- assert.ok(tile.boundingVolume?.box, `${directory}: missing content box`)
- boxVolume(tile.boundingVolume.box)
- const box = tile.boundingVolume.box
- const center = box.slice(0, 3)
- const half = [box[3], box[7], box[11]]
- const bounds = { min: center.map((value, axis) => value - half[axis]), max: center.map((value, axis) => value + half[axis]) }
- const sphere = tile.viewerRequestVolume.sphere
- const distance = record.level === 'lod1' ? manifest.budget.coarseDistance : manifest.budget.detailDistance
- assert.equal(sphere.length, 4)
- sphere.slice(0, 3).forEach((value, axis) => assert.ok(Math.abs(value - center[axis]) < 0.01, `${directory}: request center drift`))
- assert.ok(Math.abs(sphere[3] - Math.hypot(...half) - distance) < 0.01, `${directory}: wrong distance request gate`)
- const path = join(dir, tile.content.uri)
- const { bytes: raw, json, bin } = parseGlb(path)
- const nodes = json.nodes.filter((node) => node.mesh !== undefined)
- assert.equal(nodes.length, 1, `${path}: expected one mesh node`)
- assert.ok(!nodes[0].matrix && !nodes[0].rotation && !nodes[0].scale, `${path}: unexpected transform`)
- const translation = nodes[0].translation || [0, 0, 0]
- assert.equal(translation.length, 3)
- assert.ok(json.extensionsRequired?.includes('KHR_draco_mesh_compression'), `${path}: Draco is not required`)
- let fileTriangles = 0, fileVertices = 0
- for (const primitive of json.meshes[nodes[0].mesh].primitives) {
- const decoded = decodePrimitive(path, json, bin, primitive, translation, bounds)
- fileTriangles += decoded.triangles
- fileVertices += decoded.points
- }
- assert.equal(fileTriangles, record.triangles, `${path}: triangle total differs from manifest`)
- assert.equal(raw.length, record.bytes, `${path}: byte total differs from manifest`)
- assert.ok(fileVertices <= manifest.budget.maxVertices, `${path}: vertex budget exceeded`)
- byLevel[record.level].triangles += fileTriangles
- byLevel[record.level].bytes += raw.length
- assert.equal(fileVertices, record.vertices, `${path}: vertex total differs from manifest`)
- files++; triangles += fileTriangles; vertices += fileVertices; bytes += raw.length
- maxVertices = Math.max(maxVertices, fileVertices); maxTriangles = Math.max(maxTriangles, fileTriangles)
- records.delete(tile.content.uri)
- }
- assert.equal(records.size, 0, `${directory}: unreferenced manifest files`)
- for (const level of ['lod1', 'lod0']) {
- assert.deepEqual(byLevel[level], manifest.totals[level], `${directory}: ${level} totals differ from files`)
- assert.equal(byLevel[level].triangles, source.cells.reduce((sum, cell) => sum + cell[level].triangles, 0),
- `${directory}: ${level} source triangle count changed`)
- }
- const result = { directory, buildings: manifest.count, files, triangles, vertices, bytes, maxVertices, maxTriangles,
- budget: manifest.budget }
- all.push(result)
- console.log(JSON.stringify(result))
- }
- const result = { passed: true, batches: all, totals: {
- buildings: all.reduce((sum, item) => sum + item.buildings, 0),
- files: all.reduce((sum, item) => sum + item.files, 0),
- triangles: all.reduce((sum, item) => sum + item.triangles, 0),
- vertices: all.reduce((sum, item) => sum + item.vertices, 0),
- bytes: all.reduce((sum, item) => sum + item.bytes, 0),
- } }
- if (reportPath) writeFileSync(reportPath, JSON.stringify(result, null, 2) + '\n')
- console.log(JSON.stringify(result))
|