// Readback gate for the assembled roof candidate, independent of the builder's rewrites. import assert from 'node:assert/strict' import {readFileSync,writeFileSync,existsSync} from 'node:fs' import {resolve,join,dirname} from 'node:path' import {createHash} from 'node:crypto' import {unpackInstanceBuffers} from './instance-buffer-codec.mjs' import {accessorValues} from './instance-repeated-meshes.mjs' import {decodePrimitive} from './instance-safe-primitives.mjs' import {verifyMaterialClosure} from './verify-material-closure.mjs' import {tileRasterBudget} from './raster-budget.mjs' import {selectDetailRegions} from '../../src/modules/cockpit/services/regionLoading.js' const root=resolve('tools/city-massing/city/cad-bim'),base=join(root,'city-regions-v7') const dir=resolve(process.argv[2]||join(root,'city-regions-roof-pilot-v1')) const read=p=>JSON.parse(readFileSync(p)),sha=b=>createHash('sha256').update(b).digest('hex') const old=read(join(base,'manifest.json')),next=read(join(dir,'manifest.json')),candidate=next.roofQualityCandidate,pilotPath=resolve('.scratch/cockpit-shell-panel-layout/verify/artifacts/t35/roof-quality-pilot/report.json'),pilot=read(pilotPath) assert.equal(sha(readFileSync(pilotPath)),candidate.pilotReportSha256);assert.equal(pilot.passed,true);assert.equal(pilot.configurationChanged,false);assert.equal(pilot.pilotNotConfigured,true) assert.equal(sha(readFileSync(join(base,'manifest.json'))),candidate.parentManifestSha256) assert.equal(candidate.changedTiles.length,2);assert.equal(next.buildings,old.buildings) assert.deepEqual(next.transform,old.transform);assert.equal(next.regions.length,old.regions.length) const replacements=new Map(candidate.changedTiles.map(r=>[r.oldPath,r.newPath])) const used=new Set();let previews=0,fine=0,changedVerified=0 function canonicalTree(t,from,after){const clone=structuredClone(t);function walk(node){if(node.content?.uri){let path=resolve(from,node.content.uri);if(!after)path=replacements.get(path)||path;assert.ok(existsSync(path),'missing '+path);node.content.uri=path}for(const c of node.children||[])walk(c)}walk(clone.root);return clone} function rasters(decoded){const {json,bin}=decoded;return tileRasterBudget((json.images||[]).map(im=>{const v=json.bufferViews[im.bufferView];return{mimeType:im.mimeType,bytes:bin.subarray(v.byteOffset||0,(v.byteOffset||0)+v.byteLength)}}),json.textures||[])} for(const r of next.regions){ const before=old.regions.find(x=>x.id===r.id),aDir=dirname(resolve(base,before.url)),bDir=dirname(resolve(dir,r.url)) assert.equal(r.contentCount,before.contentCount);assert.equal(r.previewCount,before.previewCount) assert.deepEqual(r.admissionBounds,before.admissionBounds);assert.deepEqual(r.bounds,before.bounds);assert.deepEqual(r.sourceCells,before.sourceCells) assert.deepEqual(canonicalTree(read(resolve(base,before.url)),aDir,false),canonicalTree(read(resolve(dir,r.url)),bDir,true),'hierarchy changed '+r.id) const expected=before.contentUrls.map(uri=>{const p=resolve(aDir,uri);return replacements.get(p)||p}) const actual=r.contentUrls.map(uri=>resolve(bDir,uri));assert.deepEqual(actual,expected) assert.equal(new Set(actual).size,r.contentCount) assert.deepEqual(r.previewContentUrls.map(uri=>resolve(bDir,uri)),before.previewContentUrls.map(uri=>resolve(aDir,uri))) let delta=0 for(let i=0;ic.oldPath===oldPath);assert.ok(change) const pilotRow=pilot.results.find(x=>x.region===r.id&&x.uri===previous.uri);assert.ok(pilotRow) assert.equal(sha(readFileSync(change.sourcePath)),change.sourceSha256);assert.equal(change.sourceSha256,pilotRow.sourceSha256);assert.equal(change.sourcePath,pilotRow.sourcePath) assert.equal(record.width,change.width);assert.equal(record.height,change.height);assert.equal(record.rasterSha256,change.rasterSha256);assert.equal(record.rasterBytes,change.rasterBytes);assert.deepEqual(record.processing,change.processing);assert.deepEqual(record.sourceTiles,change.sourceTiles) assert.equal(sha(readFileSync(oldPath)),change.currentAssetSha256,'configured source changed');assert.equal(sha(readFileSync(newPath)),change.replacementSha256,'candidate replacement changed') const a=unpackInstanceBuffers(readFileSync(oldPath)),b=unpackInstanceBuffers(readFileSync(newPath));verifyMaterialClosure(b.json,newPath) assert.deepEqual(a.json.nodes,b.json.nodes);assert.equal(a.json.meshes.length,b.json.meshes.length) for(let k=0;kresolve(dir,r.uri)),old.coarseRecords.map(r=>resolve(base,r.uri))) // Exercise all two-region choices with the changed allocation included; caps must hold. let pairs=0 for(let i=0;i({...r,distance:10+k,centerDistance:10+k,retryAt:0,active:false})) const ids=selectDetailRegions(list),rs=list.filter(r=>ids.includes(r.id));assert.ok(ids.length<=2) assert.ok(rs.reduce((s,r)=>s+r.contentCount,0)<=180&&rs.reduce((s,r)=>s+r.decodedGeometryBytes,0)<=96*1024*1024) assert.deepEqual(selectDetailRegions(list,{pipeline:true}),[]);pairs++ } assert.equal(changedVerified,2);assert.equal(fine,3023);assert.equal(previews,631) const config=read('public/cockpit/config.json') assert.equal(config.map.tilesets[0].regionManifest,'/gis/cad-bim/city-regions-v7/manifest.json','candidate unexpectedly configured') const report={passed:true,regions:next.regions.length,coarseFiles:next.coarseRecords.length,previewFiles:previews,fineFiles:fine, changedFineContents:changedVerified,actualDecodedRasterDelta:candidate.changedTiles.reduce((s,c)=>s+c.deltaDecodedTextureEstimate,0), region25Estimate:next.regions.find(r=>r.id===25).decodedGeometryBytes,allPairsBudgetChecks:pairs, allUrisResolve:true,unchangedAssetsReuseExactPaths:true,geometryAndUvPreserved:true,sourceHashesMatch:true, hierarchyAndBoundsPreserved:true,pipelineRegions:0,configurationChanged:false,renderingValidated:false} if(process.argv[3])writeFileSync(process.argv[3],JSON.stringify(report,null,2));console.log(JSON.stringify(report))