| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596 |
- // Hybrid runtime tree: keep the proven 74-file far coarse layer and add only local fine leaves.
- // Fine leaves are additive siblings so a nearby detail request never removes distant coarse coverage.
- import assert from 'node:assert/strict'
- import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs'
- import { resolve, join, relative } from 'node:path'
- const root = resolve('tools/city-massing/city/cad-bim')
- const outputName = process.argv[2] || 'city-hybrid-v1'
- const output = join(root, outputName)
- assert.ok(!existsSync(join(output, 'tileset.json')), 'use a new deployment generation')
- const farDir = join(root, 'far-roofed2')
- const fineDir = join(root, 'city-continuous-v5')
- const farTree = JSON.parse(readFileSync(join(farDir, 'tileset.json'), 'utf8'))
- const fineTree = JSON.parse(readFileSync(join(fineDir, 'tileset.json'), 'utf8'))
- assert.deepEqual(farTree.root.transform, fineTree.root.transform)
- function adaptFar(tile) {
- const clone = structuredClone(tile)
- if (tile.content?.uri) clone.content.uri = relative(output, join(farDir, tile.content.uri)).replaceAll('\\', '/')
- delete clone.id
- delete clone.children
- // The far wrapper has one bounded coarse content per child. Keep that content
- // as an independent sibling so local fine leaves cannot hide other coarse areas.
- return clone
- }
- function collectFineLeaves(tile, leaves = []) {
- if (tile.content?.uri && /-lod0\.glb$/.test(tile.content.uri)) {
- const clone = structuredClone(tile)
- clone.content.uri = relative(output, join(fineDir, tile.content.uri)).replaceAll('\\', '/')
- clone.geometricError = 0
- delete clone.children
- leaves.push(clone)
- return leaves
- }
- for (const child of tile.children || []) collectFineLeaves(child, leaves)
- return leaves
- }
- function boxBounds(tile) {
- const box = tile.boundingVolume?.box
- if (box?.length >= 12) return { 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 sphere = tile.boundingVolume?.sphere
- assert.ok(sphere?.length >= 4, 'hybrid source requires box or sphere bounds')
- return { min: [sphere[0] - sphere[3], sphere[1] - sphere[3], sphere[2] - sphere[3]],
- max: [sphere[0] + sphere[3], sphere[1] + sphere[3], sphere[2] + sphere[3]] }
- }
- function center(bounds) { return bounds.min.map((value, axis) => (value + bounds.max[axis]) / 2) }
- function contains(bounds, point) { return point.every((value, axis) => value >= bounds.min[axis] && value <= bounds.max[axis]) }
- function distanceSquared(a, b) { return a.reduce((sum, value, axis) => sum + (value - b[axis]) ** 2, 0) }
- const farChildren = farTree.root.children.map(adaptFar)
- const fineLeaves = collectFineLeaves(fineTree.root)
- assert.equal(farChildren.length, 74)
- assert.equal(fineLeaves.length, 3023)
- // Attach each fine leaf beneath the coarse parent containing its center. This preserves
- // REPLACE semantics: the 74 coarse contents are the fallback until that local parent's
- // fine children are ready, rather than 3,023 coarse siblings being traversed at once.
- const parentBounds = farChildren.map(boxBounds)
- const parentCenters = parentBounds.map(center)
- for (const leaf of fineLeaves) {
- const point = center(boxBounds(leaf))
- let index = parentBounds.findIndex((bounds) => contains(bounds, point))
- if (index < 0) index = parentCenters.reduce((best, value, candidate) =>
- distanceSquared(value, point) < distanceSquared(parentCenters[best], point) ? candidate : best, 0)
- const parent = farChildren[index]
- parent.children ||= []
- parent.refine = 'REPLACE'
- parent.children.push(leaf)
- }
- const rootBox = farTree.root.boundingVolume
- mkdirSync(output, { recursive: true })
- const anchorPath = resolve(farDir, farTree.root.content.uri)
- assert.ok(existsSync(anchorPath), anchorPath)
- writeFileSync(join(output, 'root-anchor.glb'), readFileSync(anchorPath))
- writeFileSync(join(output, 'tileset.json'), JSON.stringify({
- asset: { version: '1.1', generator: 'CAD hybrid 74 coarse parents with local fine leaves' },
- geometricError: 1000000,
- root: {
- transform: farTree.root.transform,
- boundingVolume: rootBox,
- geometricError: 1000000,
- refine: 'REPLACE',
- content: { uri: 'root-anchor.glb' },
- children: farChildren,
- },
- }, null, 2) + '\n')
- const report = {
- buildings: 30172,
- farCoarseFiles: farChildren.length,
- fineFiles: fineLeaves.length,
- policy: '74-file far coarse siblings remain visible while only local GE0 fine leaves request within their bounded viewer spheres. No EXT_mesh_gpu_instancing is required; geometry is reused from immutable GLBs.',
- source: { far: 'far-roofed2', fine: 'city-continuous-v5' },
- traversalValidated: false,
- }
- writeFileSync(join(output, 'manifest.json'), JSON.stringify(report, null, 2) + '\n')
- console.log(JSON.stringify({ output, buildings: report.buildings, farCoarseFiles: report.farCoarseFiles, fineFiles: report.fineFiles }))
|