// Split verified coarse ownership into bounded independent detail regions. import assert from 'node:assert/strict' import {readFileSync,writeFileSync,mkdirSync,existsSync} from 'node:fs' import {resolve,join,relative} from 'node:path' import {addRegionMetadata} from './region-metadata.mjs' import {compressInstanceBuffers} from './instance-buffer-codec.mjs' import {parseGlb} from './instance-repeated-meshes.mjs' import {textureCoarseRoofs} from './texture-coarse-roofs.mjs' import {tileRasterBudget} from './raster-budget.mjs' const base=resolve('tools/city-massing/city/cad-bim/city-far-shell-v4'),out=resolve(process.argv[2]||'tools/city-massing/city/cad-bim/city-regions-v6') const withRoofs=process.argv.includes('--satellite-roofs') assert.ok(!existsSync(out),'use immutable generation') const tree=JSON.parse(readFileSync(join(base,'tileset.json'))),manifest=JSON.parse(readFileSync(join(base,'manifest.json'))),regions=[],coarse=[],coarseRecords=[],seen=new Set() mkdirSync(out,{recursive:true}) function fineLeaves(t,list=[]){if(t.content?.uri?.endsWith('-lod0.glb'))list.push(t);for(const c of t.children||[])fineLeaves(c,list);return list} for(let id=0;id0&&leaves.length<=128,'region content budget') let decodedGeometryBytes=0 for(const leaf of leaves){const p=resolve(base,leaf.content.uri);assert.ok(!seen.has(p),'fine repeated across regions');seen.add(p) const {json,bin}=parseGlb(readFileSync(p)) if(json.images?.length){const images=json.images.map(image=>{const v=json.bufferViews[image.bufferView];return{bytes:bin.subarray(v.byteOffset||0,(v.byteOffset||0)+v.byteLength),mimeType:image.mimeType}});decodedGeometryBytes+=tileRasterBudget(images,json.textures||[]).decodedTextureBytes} for(const mesh of json.meshes)for(const primitive of mesh.primitives){ for(const i of Object.values(primitive.attributes)){const a=json.accessors[i];decodedGeometryBytes+=a.count*({VEC2:2,VEC3:3,VEC4:4,SCALAR:1}[a.type])*4} decodedGeometryBytes+=json.accessors[primitive.indices].count*4 } } const regionDir=join(out,'region-'+id);mkdirSync(regionDir) const fineRecords=[] const fine=leaves.map((t,index)=>{ const path=resolve(base,t.content.uri),raw=readFileSync(path),{json}=parseGlb(raw) let uri=relative(regionDir,path).replaceAll('\\','/') if(withRoofs&&!json.images?.length){const result=textureCoarseRoofs(raw,{maxDimension:64}),name=`fine-${index}.glb`;writeFileSync(join(regionDir,name),compressInstanceBuffers(result.bytes));uri=name;decodedGeometryBytes+=(result.report.uvBytes||0)+(result.report.decodedTextureEstimate||0);fineRecords.push({uri:name,sourceUri:t.content.uri,...result.report})} else fineRecords.push({uri,sourceUri:t.content.uri,textured:false,preservedOriginal:true}) return{...structuredClone(t),content:{uri},geometricError:0} }) fine.forEach(t=>{delete t.children;delete t.viewerRequestVolume}) const fineMap=new Map(leaves.map((t,i)=>[resolve(base,t.content.uri),fine[i]])) const previewRecords=[],previewTiles=[] if(withRoofs){ const branches=original.content?.uri?.startsWith('shell-')?original.children:original.content?[original]:original.children for(let p=0;pstructuredClone(fineMap.get(resolve(base,t.content.uri)))) childLeaves.forEach(t=>{delete t.children;delete t.viewerRequestVolume}) previewTiles.push({boundingVolume:branch.boundingVolume,geometricError:48,refine:'REPLACE',content:{uri:name},children:childLeaves}) } } const root={transform:tree.root.transform,boundingVolume:original.boundingVolume,geometricError:1e6,refine:'REPLACE',children:withRoofs?previewTiles:fine} writeFileSync(join(regionDir,'tileset.json'),JSON.stringify({asset:{version:'1.1'},geometricError:1e6,root})) const contents=original.content?[original]:original.children for(let i=0;ip.uri), contentUrls:[...fine.map(t=>t.content.uri),...previewRecords.map(p=>p.uri)],fineRecords,sourceCells:original.content?.uri?.startsWith('shell-')?manifest.records.find(r=>r.uri===original.content.uri).sourceCells:[original.extras.fallbackSourceCell]} regions.push(info) } assert.equal(seen.size,3023) writeFileSync(join(out,'coarse-tileset.json'),JSON.stringify({asset:{version:'1.1'},geometricError:1e6,root:{...tree.root,children:coarse}})) const report={transform:tree.root.transform,buildings:manifest.buildings,regions,coarseRecords,regionCount:regions.length,fineFiles:seen.size,source:'city-far-shell-v4',maxRegionContents:Math.max(...regions.map(r=>r.contentCount)),policy:'Coarse always active; bounded independent region fine loads. Metadata.region masks coarse only after all region contents loaded and visible. Pipeline loads zero detail regions.',runtimeValidated:false} writeFileSync(join(out,'manifest.json'),JSON.stringify(report,null,2));console.log(JSON.stringify({out,regions:regions.length,maxRegionContents:report.maxRegionContents,fineFiles:seen.size,coarseFiles:coarse.length}))