build-roof-quality-region-candidate.mjs 4.6 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465
  1. // Assemble a complete regional generation with exactly the saved roof pilot overrides.
  2. // No configured path or source GLB is overwritten.
  3. import assert from 'node:assert/strict'
  4. import {readFileSync,writeFileSync,mkdirSync,copyFileSync,existsSync} from 'node:fs'
  5. import {resolve,join,relative,dirname} from 'node:path'
  6. import {createHash} from 'node:crypto'
  7. const root=resolve('tools/city-massing/city/cad-bim')
  8. const base=join(root,'city-regions-v7')
  9. const out=resolve(process.argv[2]||join(root,'city-regions-roof-pilot-v1'))
  10. const pilotPath=resolve('.scratch/cockpit-shell-panel-layout/verify/artifacts/t35/roof-quality-pilot/report.json')
  11. const configPath=resolve('public/cockpit/config.json')
  12. const sha=data=>createHash('sha256').update(data).digest('hex')
  13. const read=path=>JSON.parse(readFileSync(path))
  14. const configHash=sha(readFileSync(configPath)),pilot=read(pilotPath),manifest=read(join(base,'manifest.json'))
  15. assert.equal(pilot.passed,true);assert.equal(pilot.results.length,2)
  16. assert.ok(!existsSync(out),'use a new immutable generation')
  17. const rebased=(from,to,uri)=>relative(to,resolve(from,uri)).replaceAll('\\','/')
  18. const overrides=new Map(),changed=[]
  19. for(const entry of pilot.results){
  20. const key=entry.region+':'+entry.uri;assert.ok(!overrides.has(key));overrides.set(key,entry)
  21. assert.equal(sha(readFileSync(entry.pilotAsset)),entry.pilotAssetSha256,'pilot changed')
  22. assert.ok(entry.geometryUnchanged&&entry.uvUnchanged&&entry.facadeMaterialsUnchanged)
  23. }
  24. mkdirSync(out,{recursive:true})
  25. for(const r of manifest.regions){
  26. const oldTreePath=resolve(base,r.url),oldDir=dirname(oldTreePath)
  27. const selected=pilot.results.filter(p=>p.region===r.id)
  28. if(!selected.length){r.url=rebased(base,out,r.url);continue}
  29. const regionDir=join(out,'region-'+r.id);mkdirSync(regionDir)
  30. const tree=read(oldTreePath),replacements=new Map()
  31. for(const entry of selected){
  32. const before=r.fineRecords.find(f=>f.uri===entry.uri);assert.ok(before?.textured&&!before.preservedOriginal)
  33. const oldPath=resolve(oldDir,before.uri),newPath=join(regionDir,before.uri)
  34. assert.equal(sha(readFileSync(oldPath)),entry.currentAssetSha256,'configured source changed')
  35. copyFileSync(entry.pilotAsset,newPath);replacements.set(oldPath,newPath)
  36. Object.assign(before,entry.processed)
  37. r.decodedGeometryBytes+=entry.deltaDecodedTextureEstimate
  38. changed.push({region:r.id,uri:entry.uri,oldPath,newPath,currentAssetSha256:entry.currentAssetSha256,sourcePath:entry.sourcePath,sourceSha256:entry.sourceSha256,
  39. replacementSha256:entry.pilotAssetSha256,width:entry.processed.width,height:entry.processed.height,rasterSha256:entry.processed.rasterSha256,
  40. rasterBytes:entry.processed.rasterBytes,processing:entry.processed.processing,sourceTiles:entry.processed.sourceTiles,
  41. deltaDecodedTextureEstimate:entry.deltaDecodedTextureEstimate})
  42. }
  43. const resolveContent=uri=>{const path=resolve(oldDir,uri);return relative(regionDir,replacements.get(path)||path).replaceAll('\\','/')}
  44. function walk(t){if(t.content?.uri)t.content.uri=resolveContent(t.content.uri);for(const child of t.children||[])walk(child)}
  45. walk(tree.root)
  46. r.url=relative(out,join(regionDir,'tileset.json')).replaceAll('\\','/')
  47. r.contentUrls=r.contentUrls.map(resolveContent)
  48. r.previewContentUrls=r.previewContentUrls.map(resolveContent)
  49. for(const record of [...r.fineRecords,...r.previewRecords])record.uri=resolveContent(record.uri)
  50. assert.ok(r.decodedGeometryBytes<=96*1024*1024&&r.contentCount<=180)
  51. writeFileSync(join(regionDir,'tileset.json'),JSON.stringify(tree))
  52. }
  53. for(const r of manifest.coarseRecords)r.uri=rebased(base,out,r.uri)
  54. const coarse=read(join(base,'coarse-tileset.json'))
  55. function walkCoarse(t){if(t.content?.uri)t.content.uri=rebased(base,out,t.content.uri);for(const c of t.children||[])walkCoarse(c)}
  56. walkCoarse(coarse.root)
  57. manifest.assetBase='city-regions-v6'
  58. manifest.roofQualityCandidate={parentGeneration:'city-regions-v7',parentManifestSha256:sha(readFileSync(join(base,'manifest.json'))),
  59. pilotReportSha256:sha(readFileSync(pilotPath)),changedTiles:changed,configured:false,renderingValidated:false,
  60. policy:'Same regions/preview/bounds/LOD/geometry. Only two sourced roof rasters enhanced; estimate updated before admission.'}
  61. writeFileSync(join(out,'coarse-tileset.json'),JSON.stringify(coarse))
  62. writeFileSync(join(out,'manifest.json'),JSON.stringify(manifest,null,2))
  63. assert.equal(sha(readFileSync(configPath)),configHash,'configuration unexpectedly changed')
  64. console.log(JSON.stringify({passed:true,out,regions:manifest.regions.length,changedFineContents:changed.length,
  65. changedRegionEstimate:manifest.regions.find(r=>r.id===25).decodedGeometryBytes,configurationChanged:false}))