| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102 |
- // One formal city tree: cell-owned distant coarse -> bounded local coarse/fine pairs.
- // Use instance candidates only where offline payload AND primitive counts beat the
- // old fine partitions; otherwise reuse the original immutable branch.
- import assert from 'node:assert/strict'
- import {readFileSync,writeFileSync,mkdirSync,existsSync} from 'node:fs'
- import {resolve,join,relative} from 'node:path'
- import {parseGlb} from './instance-repeated-meshes.mjs'
- const root=resolve('tools/city-massing/city/cad-bim'),inst=resolve(process.argv[2]),out=resolve(process.argv[3])
- assert.ok(!existsSync(out),'immutable output exists')
- const im=JSON.parse(readFileSync(join(inst,'manifest.json'))),it=JSON.parse(readFileSync(join(inst,'tileset.json')))
- const current=JSON.parse(readFileSync(join(root,'city-continuous-v5/manifest.json')))
- const groups=new Map(),nodes=new Map(it.root.children.map(t=>[t.children[0].content.uri,t]))
- for(const c of im.cells){if(!groups.has(c.sourceCell))groups.set(c.sourceCell,[]);groups.get(c.sourceCell).push(c)}
- const children=[],coverage=new Set(),records=[],decisions=[]
- let farFiles=0,nearFiles=0,fineFiles=0
- function load(p){return JSON.parse(readFileSync(p))}
- function uri(dir,p){return relative(out,resolve(dir,p)).replaceAll('\\','/')}
- function rebase(tile,dir){
- const t=structuredClone(tile)
- if(t.content)t.content.uri=uri(dir,t.content.uri)
- if(t.children?.length){
- t.children=t.children.map(c=>rebase(c,dir))
- if(t.boundingVolume.box){const b=t.boundingVolume.box;t.boundingVolume={sphere:[...b.slice(0,3),Math.hypot(b[3],b[7],b[11])]}}
- t.geometricError=1000000;t.refine='REPLACE'
- }
- return t
- }
- function countPrimitives(path){const {json}=parseGlb(readFileSync(path));return json.nodes.reduce((s,n)=>s+(n.mesh===undefined?0:json.meshes[n.mesh].primitives.length),0)}
- function sphere(bounds){const center=bounds.min.map((v,a)=>(v+bounds.max[a])/2),half=bounds.min.map((v,a)=>(bounds.max[a]-v)/2);return [...center,Math.hypot(...half)]}
- for(const s of current.sources){
- const dir=join(root,s.source),sm=load(join(dir,'manifest.json')),st=load(join(dir,'tileset.json'))
- const batch=s.source.split('/')[0],baseDir=join(root,batch),base=load(join(baseDir,'manifest.json'))
- const baseCells=new Map(base.cells.map(c=>[c.id,c]))
- assert.equal(sm.cells.length,st.root.children.length)
- for(let index=0;index<sm.cells.length;index++){
- const original=sm.cells[index],baseCell=baseCells.get(original.id)
- assert.ok(baseCell)
- for(const id of original.buildingIds){assert.ok(!coverage.has(id));coverage.add(id)}
- const key=batch.replace(/^lod-/,'').replace(/-roofed2$/,'')+'-'+original.id
- const candidates=groups.get(key)||[]
- const oldFiles=[]
- function fine(t){if(t.content?.uri?.endsWith('-lod0.glb'))oldFiles.push(t.content.uri);for(const c of t.children||[])fine(c)}
- fine(st.root.children[index])
- const oldBytes=oldFiles.reduce((a,p)=>a+readFileSync(join(dir,p)).length,0)
- const oldPrimitives=oldFiles.reduce((a,p)=>a+countPrimitives(join(dir,p)),0)
- const candidateBytes=candidates.reduce((a,c)=>a+c.lod0.bytes,0)
- const candidatePrimitives=candidates.reduce((a,c)=>a+c.lod0.drawPrimitives,0)
- const candidateIds=candidates.flatMap(c=>c.buildingIds),candidateSet=new Set(candidateIds)
- const exactCoverage=candidateSet.size===candidateIds.length&&candidateSet.size===original.buildingIds.length&&original.buildingIds.every(i=>candidateSet.has(i))
- const use=exactCoverage&&candidates.some(c=>c.instanceGroups>0)&&candidateBytes<oldBytes&&candidatePrimitives<=oldPrimitives
- let local=use?candidates.map(c=>rebase(nodes.get(c.lod0.uri),inst)):rebase(st.root.children[index],dir).children
- const bounds=structuredClone(baseCell.boundsENU)
- if(use)for(const c of candidates)for(let a=0;a<3;a++){
- bounds.min[a]=Math.min(bounds.min[a],c.boundsENU.min[a]);bounds.max[a]=Math.max(bounds.max[a],c.boundsENU.max[a])
- }
- const volume=sphere(bounds),coarseWhole=baseCell.lod1.triangles<=20000
- if(coarseWhole){
- // Admit all local coarse branches at one shared camera gate, protecting
- // residual coverage when only some fine request volumes are admitted.
- const gate={sphere:[...volume.slice(0,3),volume[3]+900]}
- for(const t of local){if(t.children?.length)t.viewerRequestVolume=structuredClone(gate)}
- const outer={boundingVolume:{sphere:volume},geometricError:1000000,refine:'REPLACE',
- viewerRequestVolume:{sphere:[...volume.slice(0,3),volume[3]+6000]},
- content:{uri:uri(baseDir,baseCell.lod1.uri)},children:[{
- boundingVolume:{sphere:volume},geometricError:1000000,refine:'REPLACE',viewerRequestVolume:gate,children:local}]}
- children.push(outer);farFiles++
- records.push({uri:outer.content.uri,triangles:baseCell.lod1.triangles,level:'far',buildingIds:original.buildingIds})
- }else{
- // Exactly two legacy coarse cells exceed the whole-cell budget; retain
- // bounded partition granularity rather than exporting a larger GLB.
- children.push({boundingVolume:{sphere:volume},geometricError:1000000,refine:'REPLACE',children:local})
- }
- const localOwners=new Map()
- for(const t of local){
- const uri=t.content?.uri
- const owner=use?candidates.find(c=>uri?.endsWith('/'+c.lod1.uri.split('/').at(-1))):null
- if(use)assert.ok(owner,'missing candidate pair ownership')
- localOwners.set(t,owner?owner.buildingIds:original.buildingIds)
- }
- function collect(t,ownerIds){if(t.content){const level=t.children?.length?'near':'fine';if(level==='near')nearFiles++;else fineFiles++;
- records.push({uri:t.content.uri,level,sourceCell:key,buildingIds:ownerIds})}for(const c of t.children||[])collect(c,ownerIds)}
- local.forEach(t=>collect(t,localOwners.get(t)))
- decisions.push({cell:key,buildingIds:original.buildingIds,useInstancing:use,oldBytes,candidateBytes,oldPrimitives,candidatePrimitives,
- instanceGroups:use?candidates.reduce((s,c)=>s+c.instanceGroups,0):0,instancedBuildings:use?candidates.reduce((s,c)=>s+c.instancedBuildings,0):0,
- wholeCellCoarse:coarseWhole})
- }
- }
- assert.equal(coverage.size,30172)
- mkdirSync(out,{recursive:true})
- const currentTree=load(join(root,'city-continuous-v5/tileset.json'))
- writeFileSync(join(out,'tileset.json'),JSON.stringify({asset:{version:'1.1',generator:'Cell coarse + bounded translation-instance candidates'},geometricError:1000000,
- root:{transform:currentTree.root.transform,boundingVolume:currentTree.root.boundingVolume,geometricError:1000000,refine:'REPLACE',children}},null,2))
- const report={buildings:coverage.size,farFiles,nearFiles,fineFiles,decisions,records,sourceInstancing:inst,
- retainedCells:decisions.filter(d=>!d.useInstancing).length,
- payloadBeforeAcceptedCells:decisions.filter(d=>d.useInstancing).reduce((s,d)=>s+d.oldBytes,0),
- payloadAfterAcceptedCells:decisions.filter(d=>d.useInstancing).reduce((s,d)=>s+d.candidateBytes,0),
- primitivesBeforeAcceptedCells:decisions.filter(d=>d.useInstancing).reduce((s,d)=>s+d.oldPrimitives,0),
- primitivesAfterAcceptedCells:decisions.filter(d=>d.useInstancing).reduce((s,d)=>s+d.candidatePrimitives,0),
- candidateOnly:true,instancedCells:decisions.filter(d=>d.useInstancing).length,instancedBuildings:decisions.reduce((s,d)=>s+d.instancedBuildings,0),
- policy:'Far coarse +6000m, whole-cell shared local coarse gate +900m, original fine +250/new +350m; bounding sphere half-diagonal expands these camera gates. Normal-invariant shared primitives retain exact TRS, unsafe slopes keep ordinary transforms. No runtime/FPS claim.'}
- writeFileSync(join(out,'manifest.json'),JSON.stringify(report,null,2))
- console.log(JSON.stringify({out,buildings:report.buildings,farFiles,nearFiles,fineFiles,instancedCells:report.instancedCells,instancedBuildings:report.instancedBuildings}))
|