| 123456789101112131415161718192021222324252627282930313233343536373839404142 |
- // Offline pose sweep using recorded HPR and installed Cesium camera/frustum math.
- import assert from 'node:assert/strict'
- import {readFileSync,writeFileSync} from 'node:fs'
- import {resolve,join} from 'node:path'
- import {Matrix4,Matrix3,Quaternion,Cartesian3,BoundingSphere,Transforms,PerspectiveFrustum,Intersect} from 'cesium'
- import {contentAdmission,selectDetailRegions,REGION_BUDGET_DEFAULTS} from '../../src/modules/cockpit/services/regionLoading.js'
- const root=resolve('tools/city-massing/city/cad-bim'),v6=JSON.parse(readFileSync(join(root,'city-regions-v6/manifest.json'))),v7=JSON.parse(readFileSync(join(root,'city-regions-v7/manifest.json'))),transform=Matrix4.fromArray(v7.transform)
- const pivot={lon:110.39028164000001,lat:28.4436787,height:111.57951503970997},radians=n=>n*Math.PI/180
- function camera(heading,pitch,range){
- const target=Cartesian3.fromDegrees(pivot.lon,pivot.lat,pivot.height),local=Transforms.eastNorthUpToFixedFrame(target)
- const qp=Quaternion.fromAxisAngle(Cartesian3.UNIT_Y,-radians(pitch)),qh=Quaternion.fromAxisAngle(Cartesian3.UNIT_Z,-(radians(heading)-Math.PI/2)),rot=Matrix3.fromQuaternion(Quaternion.multiply(qh,qp,new Quaternion()))
- const offset=Matrix3.multiplyByVector(rot,Cartesian3.UNIT_X,new Cartesian3());Cartesian3.multiplyByScalar(offset,-range,offset)
- const direction=Cartesian3.normalize(Cartesian3.negate(offset,new Cartesian3()),new Cartesian3()),right=Cartesian3.normalize(Cartesian3.cross(direction,Cartesian3.UNIT_Z,new Cartesian3()),new Cartesian3()),up=Cartesian3.normalize(Cartesian3.cross(right,direction,new Cartesian3()),new Cartesian3())
- return {position:Matrix4.multiplyByPoint(local,offset,new Cartesian3()),direction:Matrix4.multiplyByPointAsVector(local,direction,new Cartesian3()),up:Matrix4.multiplyByPointAsVector(local,up,new Cartesian3())}
- }
- function sphere(volume){assert.ok(volume.sphere);const s=volume.sphere;return new BoundingSphere(Matrix4.multiplyByPoint(transform,new Cartesian3(...s.slice(0,3)),new Cartesian3()),s[3])}
- const samples=[]
- for(const range of [12000,1560.344530541111,469.96674118784495,180,120])for(const pitch of [-55,-18])for(const heading of [0,90,180,270]){
- const pose=camera(heading,pitch,range),frustum=new PerspectiveFrustum({fov:Math.PI/3,aspectRatio:1440/810,near:1,far:14000}),volume=frustum.computeCullingVolume(pose.position,pose.direction,pose.up)
- const visible=s=>volume.computeVisibility(s)!==Intersect.OUTSIDE,dist=s=>Cartesian3.distance(pose.position,s.center)
- function candidates(manifest,useContent){return manifest.regions.map(r=>{const actual=(v7.regions[r.id].admissionBounds).map(sphere),bounds=(useContent?r.admissionBounds:[r.bounds]).map(sphere),admission=contentAdmission(bounds,visible,dist);return{...r,...admission,retryAt:0,active:false,actualVisibleContents:actual.filter(visible).length}})}
- const old=candidates(v6,false),next=candidates(v7,true),selected=selectDetailRegions(next),records=selected.map(id=>next.find(r=>r.id===id)),oldSelected=selectDetailRegions(old)
- assert.ok(records.every(r=>r.actualVisibleContents>0))
- // 票 61:选择器从"最多 2 个 / 180 条 / 96MiB"改为**半径 + 预算**驱动(控制量是覆盖半径,
- // 不再是区域个数 —— 按个数切会在同距离相邻区域之间留下硬边,正是"一半精模一半火柴盒"的成因)。
- // 这里改为断言新口径的不变量:不超新预算、且选中的都是"真的有可见内容"的区域。
- assert.ok(records.reduce((s,r)=>s+r.contentCount,0)<=REGION_BUDGET_DEFAULTS.maxContents&&records.reduce((s,r)=>s+r.decodedGeometryBytes,0)<=REGION_BUDGET_DEFAULTS.maxDecodedBytes)
- assert.deepEqual(selectDetailRegions(next,{pipeline:true}),[])
- samples.push({heading,pitch,range,cameraECEF:{position:[pose.position.x,pose.position.y,pose.position.z],direction:[pose.direction.x,pose.direction.y,pose.direction.z],up:[pose.up.x,pose.up.y,pose.up.z]},oldRegions:oldSelected,oldSelectedWithNoActualVisibleContent:oldSelected.filter(id=>!old.find(r=>r.id===id).actualVisibleContents),newRegions:selected,newVisibleContentBounds:records.map(r=>({id:r.id,visibleBounds:r.actualVisibleContents,distance:r.distance})),contents:records.reduce((s,r)=>s+r.contentCount,0),decodedEstimate:records.reduce((s,r)=>s+r.decodedGeometryBytes,0),pipelineRegions:0})
- }
- const recorded=samples.find(s=>s.heading===0&&s.pitch===-55&&s.range===469.96674118784495)
- // 票 61:这一条原本钉住"旧选择器在该机位选出 2 个**实际没有可见内容**的区域"。
- // 新选择器按半径覆盖、且以内容表面距排序,选出的集合必然不同 —— 该 fixture 的**旧语义**
- // 不再可用。保留其真实意图(新口径下不得选到"零可见内容"的区域),改为不变量断言:
- for(const row of recorded.newVisibleContentBounds)assert.ok(row.visibleBounds>0,'no selected region may have zero visible content (id '+row.id+')')
- // 票 61:旧断言写死 [78,77](当时 maxRegions=2 的产物)。新口径按半径覆盖,
- // 同一机位会把附近**所有**在覆盖范围内的可见内容区都选中 —— 改为验证不变量。
- assert.ok(recorded.newRegions.includes(78)&&recorded.newRegions.includes(77),'actual-content admission still selects the nearby visible bounds')
- assert.ok(recorded.newRegions.length>=2,'radius coverage must not shrink below the old pair at this pose')
- for(const row of recorded.newVisibleContentBounds)assert.ok(row.visibleBounds>0,'every selected region must have actually visible content (id '+row.id+')')
- const report={passed:true,recordedPivot:pivot,samples,frustumAssumption:'60 degree FOV, 1440x810 canvas; actual browser FOV not captured',activeHistoryApplied:false,renderingValidated:false,gpuMeasured:false,scope:'Cesium HPR/frustum math, selected content-bounds visibility and declared aggregate budgets only'}
- writeFileSync(process.argv[2]||'.scratch/cockpit-shell-panel-layout/verify/artifacts/t35/admission-pose-sweep-v7.json',JSON.stringify(report,null,2));console.log(JSON.stringify({passed:true,poses:samples.length,oldEmptySelections:samples.reduce((s,r)=>s+r.oldSelectedWithNoActualVisibleContent.length,0),newEmptySelections:0,recordedHeading0:samples.filter(s=>s.heading===0&&s.pitch===-55),renderingValidated:false}))
|