visibility.html 8.5 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091
  1. <!doctype html>
  2. <html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>管段可见性验证</title><style>html,body,#map{margin:0;width:100%;height:100%;overflow:hidden;background:#04121f;color:white;font:14px sans-serif}aside{position:absolute;top:8px;left:8px;z-index:5;background:#04121fe8;padding:10px;max-width:90vw}button{padding:8px;margin:3px}pre{font:12px monospace;white-space:pre-wrap;max-height:24vh;overflow:auto}</style></head><body><div id="map"></div><aside><strong>真实管段可见性:颜色像素与管体就绪分开核验</strong><br><button id="load">加载真实管段近景</button><button id="scan">测量管体颜色像素</button><button id="ground">地面遮挡</button><button id="pipeline">地下透视</button><button id="fps">10秒运动FPS</button><button id="mapfps">仅地图FPS(隐藏管段)</button><button id="emptyfps">空场景FPS</button><button id="raf">浏览器帧调度</button><button id="far">远景800m</button><pre id="state">准备</pre><pre id="pixels"></pre><pre id="perf"></pre></aside><script type="module">
  3. window.CESIUM_BASE_URL='/cesium/'
  4. const transformed=await fetch('/src/modules/cockpit/services/cesiumPipeModels.js').then(r=>r.text())
  5. const cesiumUrl=transformed.match(/from ["']([^"']*\/cesium\.js[^"']*)["']/)?.[1]||'/.cache/deps/cesium.js'
  6. const {Cartesian3,Cartographic,HeadingPitchRange,sampleTerrainMostDetailed,TileMapServiceImageryProvider,ImageryLayer,UrlTemplateImageryProvider}=await import(cesiumUrl)
  7. const {createViewer,applyTerrain,applyViewModeLayers}=await import('/src/modules/cockpit/services/cesiumEngine.js')
  8. const {planViewMode}=await import('/src/modules/cockpit/domain/viewModes.js')
  9. const {syncPipeLayer,clearPipeLayer,PIPE_SOURCE_NAME}=await import('/src/modules/cockpit/services/cesiumPipeLayer.js')
  10. const {toPipeNetwork}=await import('/src/modules/cockpit/domain/pipeNetwork.js')
  11. const cfg=await fetch('/cockpit/config.json').then(r=>r.json())
  12. const viewer=createViewer(document.querySelector('#map'),{ready:true,kind:'tms',url:'Assets/Textures/NaturalEarthII'})
  13. await applyTerrain(viewer,{ready:true,url:cfg.map.terrain.url,requestVertexNormals:true})
  14. for(const overlay of cfg.map.overlays||[]) viewer.imageryLayers.add(new ImageryLayer(new UrlTemplateImageryProvider({url:overlay.url})))
  15. let target=null,latest=null,network=null,range=160
  16. const renderState=()=>{const {covered=[],...counts}=latest||{};document.querySelector('#state').textContent=JSON.stringify({target:target?.code,range,canvas:[viewer.canvas.width,viewer.canvas.height],...counts,covered:covered.length},null,0)}
  17. const look=(heading=0)=>{if(target)viewer.camera.lookAt(Cartesian3.fromDegrees(target.lon,target.lat,target.height),new HeadingPitchRange(heading,-Math.PI/4,range));viewer.scene.requestRender()}
  18. const mode=(value)=>{applyViewModeLayers(viewer,[],planViewMode(value));viewer.scene.requestRender()}
  19. document.querySelector('#load').onclick=async()=>{
  20. const envelope=await fetch(cfg.api.baseUrl+'/infrastructure/geometry').then(r=>r.json())
  21. network=toPipeNetwork(envelope.data)
  22. const pipe=network.pipes.find(p=>p.diameter>0&&p.industry==='DRAINAGE'&&/污水/.test(p.remark)&&!/雨水/.test(p.remark))
  23. const a=pipe.positions[0],b=pipe.positions.at(-1)
  24. const lon=(a[0]+b[0])/2,lat=(a[1]+b[1])/2
  25. const [ground]=await sampleTerrainMostDetailed(viewer.terrainProvider,[Cartographic.fromDegrees(lon,lat)])
  26. target={code:pipe.code,lon,lat,height:ground.height}
  27. look();mode('pipeline')
  28. await syncPipeLayer(viewer,network,{onModelChange:s=>{latest=s;renderState()}})
  29. }
  30. document.querySelector('#far').onclick=()=>{range=800;look();renderState()}
  31. document.querySelector('#ground').onclick=()=>mode('ground')
  32. document.querySelector('#pipeline').onclick=()=>mode('pipeline')
  33. document.querySelector('#scan').onclick=()=>{
  34. const source=viewer.dataSources.getByName(PIPE_SOURCE_NAME)[0]
  35. const wasShown=source.show;source.show=false
  36. const models=[]
  37. for(let i=0;i<viewer.scene.primitives.length;i++){const p=viewer.scene.primitives.get(i);if(p.id?.kind==='pipe-model-batch')models.push(p)}
  38. const prior=models.map(m=>m.show)
  39. const readPixels=()=>{const canvas=document.createElement('canvas');canvas.width=viewer.canvas.width;canvas.height=viewer.canvas.height;const ctx=canvas.getContext('2d',{willReadFrequently:true});ctx.drawImage(viewer.canvas,0,0);return ctx.getImageData(0,0,canvas.width,canvas.height).data}
  40. let frames=0,baseline=null
  41. for(const m of models)m.show=false
  42. const detach=viewer.scene.postRender.addEventListener(()=>{
  43. if(++frames<3){viewer.scene.requestRender();return}
  44. if(!baseline){baseline=readPixels();models.forEach((m,i)=>m.show=prior[i]);frames=0;viewer.scene.requestRender();return}
  45. const data=readPixels(),counts={red:0,blue:0,green:0,cyan:0,purple:0};let changed=0
  46. for(let i=0;i<data.length;i+=4){
  47. const r=data[i],g=data[i+1],b=data[i+2]
  48. if(Math.abs(r-baseline[i])+Math.abs(g-baseline[i+1])+Math.abs(b-baseline[i+2])<35)continue
  49. changed++
  50. if(r>130&&r>g*1.7&&r>b*1.4)counts.red++
  51. if(b>130&&b>r*1.7&&b>g*1.15)counts.blue++
  52. if(g>130&&g>r*1.7&&g>b*1.4)counts.green++
  53. if(g>130&&b>130&&r<90&&Math.abs(g-b)<80)counts.cyan++
  54. if(r>100&&b>130&&g<100&&b>g*1.4)counts.purple++
  55. }
  56. const result={target:target.code,modelOnly:true,backgroundSubtracted:true,canvas:[viewer.canvas.width,viewer.canvas.height],range,terrainAlpha:viewer.scene.globe.translucency.frontFaceAlpha,counts,changed,pass:counts.green>=80,expected:'sewage glTF green difference pixels >=80; entities/background excluded',visible:document.visibilityState}
  57. document.querySelector('#pixels').textContent=JSON.stringify(result,null,2)
  58. document.body.dataset.pixelPass=result.pass
  59. source.show=wasShown;detach();viewer.scene.requestRender()
  60. });viewer.scene.requestRender()
  61. }
  62. function benchmark(testMode='full') {
  63. const gl=viewer.canvas.getContext('webgl2')||viewer.canvas.getContext('webgl'),debug=gl.getExtension('WEBGL_debug_renderer_info')
  64. const environment={renderer:debug?gl.getParameter(debug.UNMASKED_RENDERER_WEBGL):gl.getParameter(gl.RENDERER),vendor:debug?gl.getParameter(debug.UNMASKED_VENDOR_WEBGL):gl.getParameter(gl.VENDOR),userAgent:navigator.userAgent,devicePixelRatio,focused:document.hasFocus(),hardwareConcurrency:navigator.hardwareConcurrency}
  65. const old=viewer.scene.requestRenderMode,globeShow=viewer.scene.globe.show,models=[],source=viewer.dataSources.getByName(PIPE_SOURCE_NAME)[0],sourceShow=source?.show
  66. for(let i=0;i<viewer.scene.primitives.length;i++){const p=viewer.scene.primitives.get(i);if(p.id?.kind==='pipe-model-batch')models.push([p,p.show])}
  67. if(testMode!=='full'){models.forEach(([p])=>p.show=false);if(source)source.show=false}
  68. if(testMode==='empty')viewer.scene.globe.show=false
  69. viewer.scene.requestRenderMode=false
  70. const times=[],start=performance.now(),focusStart=document.hasFocus();let hidden=false,focusLost=false
  71. const detach=viewer.scene.postRender.addEventListener(()=>{
  72. const now=performance.now();times.push(now);hidden ||= document.visibilityState!=='visible';focusLost ||= !document.hasFocus()
  73. if(now-start<10000){look((now-start)/10000*Math.PI/4);return}
  74. detach();viewer.scene.requestRenderMode=old;viewer.scene.globe.show=globeShow;models.forEach(([p,show])=>p.show=show);if(source)source.show=sourceShow
  75. const gaps=times.slice(1).map((t,i)=>t-times[i]).sort((a,b)=>a-b),seconds=(times.at(-1)-times[0])/1000
  76. document.querySelector('#perf').textContent=JSON.stringify({testMode,frames:times.length,seconds,fps:(times.length-1)/seconds,p95FrameMs:gaps[Math.floor(gaps.length*.95)],maxFrameMs:gaps.at(-1),canvas:[viewer.canvas.width,viewer.canvas.height],viewport:[innerWidth,innerHeight],visible:document.visibilityState,hidden,focusStart,focusLost,environment,motion:'45deg/10s'},null,2)
  77. viewer.scene.requestRender()
  78. })
  79. }
  80. document.querySelector('#fps').onclick=()=>benchmark('full')
  81. document.querySelector('#mapfps').onclick=()=>benchmark('map-only')
  82. document.querySelector('#emptyfps').onclick=()=>benchmark('empty')
  83. document.querySelector('#raf').onclick=()=>{
  84. const start=performance.now(),times=[]
  85. function frame(now){times.push(now);if(now-start<5000){requestAnimationFrame(frame);return}
  86. const span=(times.at(-1)-times[0])/1000
  87. document.querySelector('#perf').textContent=JSON.stringify({testMode:'native-rAF',frames:times.length,seconds:span,fps:(times.length-1)/span,visible:document.visibilityState,focused:document.hasFocus(),canvas:[viewer.canvas.width,viewer.canvas.height]},null,2)}
  88. requestAnimationFrame(frame)
  89. }
  90. document.querySelector('#state').textContent='引擎就绪;选择加载真实管段近景'
  91. </script></body></html>