cockpitAcceptance.js 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  1. // Temporary QA overlay mounted only for ?pipeVerify=1 on the shared cockpit route.
  2. // It exercises the actual scene; it is removed from mapService before delivery.
  3. import { Cartesian3, Cartographic, HeadingPitchRange, sampleTerrainMostDetailed } from 'cesium'
  4. import { toPipeNetwork } from '../../src/modules/cockpit/domain/pipeNetwork.js'
  5. import { pipeColorKey } from '../../src/modules/cockpit/domain/pipeVisual.js'
  6. export function mountPipeAcceptance({ viewer, host, setViewMode, applyViewPreset }) {
  7. const params=new URLSearchParams(location.search)
  8. const originalFetch=window.fetch.bind(window),missingKind=params.get('pipeMissing')
  9. if (['valve','elbow','branch'].includes(missingKind)) {
  10. window.fetch=(input,options)=>String(input).endsWith(`/models/pipes/${missingKind}.gltf`)
  11. ? Promise.resolve(new Response('isolated missing template',{status:404})) : originalFetch(input,options)
  12. }
  13. const panel = document.createElement('aside')
  14. panel.id = 'pipe-acceptance-overlay'
  15. panel.style.cssText = 'position:fixed;left:25%;bottom:12px;z-index:1200;max-width:55vw;max-height:36vh;overflow:auto;padding:10px;background:#04121feb;color:white;font:13px monospace;border:1px solid #32c5ff'
  16. panel.innerHTML = '<b>管体验收(开发隔离控件)</b><div></div><pre style="white-space:pre-wrap"></pre>'
  17. const buttons = panel.querySelector('div'), output = panel.querySelector('pre')
  18. let network, target, range = 160, heading = 0
  19. const results = {}
  20. const emit = (key, value) => { results[key] = value; output.textContent = JSON.stringify(results, null, 2) }
  21. const add = (name, action) => {
  22. const button = document.createElement('button')
  23. button.textContent = name; button.style.cssText = 'margin:3px;padding:5px;color:#fff;background:#12426a;border:1px solid #328abc'
  24. button.onclick = () => Promise.resolve(action()).catch(error => emit('error', String(error.message || error)))
  25. buttons.appendChild(button)
  26. }
  27. const pipes = async () => {
  28. if (!network) {
  29. const config = await fetch('/cockpit/config.json').then(r => r.json())
  30. const payload = await fetch(config.api.baseUrl + '/infrastructure/geometry').then(r => r.json())
  31. if (payload.code !== 200) throw new Error('geometry business error')
  32. network = toPipeNetwork(payload.data)
  33. }
  34. return network
  35. }
  36. const aim = async (key) => {
  37. const data = await pipes()
  38. const row = key === 'MANHOLE' ? data.components.find(p => p.industry === 'MANHOLE') : data.pipes.find(p => p.diameter > 0 && pipeColorKey(p) === key)
  39. if (!row) throw new Error('No real target ' + key)
  40. const a = row.position || row.positions[0], b = row.position || row.positions.at(-1)
  41. const lon = (a[0]+b[0])/2, lat = (a[1]+b[1])/2
  42. const [point] = await sampleTerrainMostDetailed(viewer.terrainProvider, [Cartographic.fromDegrees(lon,lat)])
  43. target = {code:row.code,key,lon,lat,height:point.height}
  44. setViewMode('pipeline'); heading=0
  45. applyViewPreset({lon,lat,height:point.height},{heading:0,pitch:-45,range},{immediate:true})
  46. viewer.scene.requestRender()
  47. emit('target', {...target,range,remark:row.remark||'',canvas:[viewer.canvas.width,viewer.canvas.height]})
  48. }
  49. const frame = () => new Promise(resolve => {
  50. const detach = viewer.scene.postRender.addEventListener(() => { detach(); resolve() })
  51. viewer.scene.requestRender()
  52. })
  53. const capture = () => {
  54. const canvas=document.createElement('canvas');canvas.width=viewer.canvas.width;canvas.height=viewer.canvas.height
  55. const ctx=canvas.getContext('2d',{willReadFrequently:true});ctx.drawImage(viewer.canvas,0,0)
  56. return ctx.getImageData(0,0,canvas.width,canvas.height).data
  57. }
  58. const modelList = () => {
  59. const models=[]
  60. 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)}
  61. return models
  62. }
  63. add('真实污水近景',()=>aim('SEWAGE'))
  64. add('真实雨水近景',()=>aim('RAINWATER'))
  65. add('真实燃气近景',()=>aim('GAS'))
  66. add('真实供水近景',()=>aim('WATER_SUPPLY'))
  67. add('窨井紫色',()=>aim('MANHOLE'))
  68. add('远景800m',async()=>{range=800;await aim(target?.key||'SEWAGE')})
  69. add('近景160m',async()=>{range=160;await aim(target?.key||'SEWAGE')})
  70. add('测管体像素',async()=>{
  71. if(!target)throw new Error('Choose target')
  72. const source=viewer.dataSources.getByName('cockpit-pipelines')[0],shown=source.show
  73. const models=modelList(),states=models.map(m=>m.show)
  74. source.show=false;models.forEach(m=>m.show=false)
  75. try{
  76. await frame();await frame();const before=capture()
  77. models.forEach((m,i)=>m.show=states[i]);await frame();await frame();const after=capture()
  78. const count={GAS:0,WATER_SUPPLY:0,SEWAGE:0,RAINWATER:0};let changed=0
  79. for(let i=0;i<after.length;i+=4){const r=after[i],g=after[i+1],b=after[i+2];if(Math.abs(r-before[i])+Math.abs(g-before[i+1])+Math.abs(b-before[i+2])<35)continue;changed++
  80. if(r>120&&r>g*1.7&&r>b*1.35)count.GAS++
  81. if(b>120&&b>r*1.7&&b>g*1.15)count.WATER_SUPPLY++
  82. if(g>120&&g>r*1.7&&g>b*1.3)count.SEWAGE++
  83. if(g>120&&b>120&&r<100&&Math.abs(g-b)<90)count.RAINWATER++}
  84. emit('pixels',{modelOnly:true,backgroundSubtracted:true,target:target.code,color:target.key,counts:count,changed,pass:(count[target.key]||0)>=80,canvas:[viewer.canvas.width,viewer.canvas.height]})
  85. }finally{models.forEach((m,i)=>m.show=states[i]);source.show=shown;viewer.scene.requestRender()}
  86. })
  87. add('原生rAF',()=>{
  88. const start=performance.now(),times=[]
  89. const next=t=>{times.push(t);if(t-start<5000){requestAnimationFrame(next);return}const secs=(times.at(-1)-times[0])/1000;emit('raf',{frames:times.length,seconds:secs,fps:(times.length-1)/secs,visible:document.visibilityState,focused:document.hasFocus()})}
  90. requestAnimationFrame(next)
  91. })
  92. add('4K运动FPS',()=>{
  93. if(!target)throw new Error('Choose target')
  94. const scene=viewer.scene,old=scene.requestRenderMode,start=performance.now(),times=[];let hidden=false,lostFocus=false
  95. scene.requestRenderMode=false
  96. const detach=scene.postRender.addEventListener(()=>{
  97. const now=performance.now();times.push(now);hidden ||= document.visibilityState!=='visible';lostFocus ||= !document.hasFocus()
  98. if(now-start<10000){heading=(now-start)/10000*Math.PI/4;applyViewPreset({lon:target.lon,lat:target.lat,height:target.height},{heading:heading*180/Math.PI,pitch:-45,range},{immediate:true});return}
  99. detach();scene.requestRenderMode=old
  100. const gaps=times.slice(1).map((t,i)=>t-times[i]).sort((a,b)=>a-b),secs=(times.at(-1)-times[0])/1000
  101. const gl=viewer.canvas.getContext('webgl2')||viewer.canvas.getContext('webgl'),debug=gl.getExtension('WEBGL_debug_renderer_info')
  102. emit('fps',{frames:times.length,seconds:secs,fps:(times.length-1)/secs,p95:gaps[Math.floor(gaps.length*.95)],max:gaps.at(-1),canvas:[viewer.canvas.width,viewer.canvas.height],hidden,lostFocus,renderer:debug?gl.getParameter(debug.UNMASKED_RENDERER_WEBGL):gl.getParameter(gl.RENDERER),pass:(times.length-1)/secs>=30})
  103. })
  104. })
  105. add('隔离四类样例',async()=>{
  106. const [{CustomDataSource,Color,HeightReference},{createCesiumPipeModels}]=await Promise.all([import('cesium'),import('../../src/modules/cockpit/services/cesiumPipeModels.js')])
  107. const lon=110.39028,lat=28.44368
  108. const [ground]=await sampleTerrainMostDetailed(viewer.terrainProvider,[Cartographic.fromDegrees(lon,lat)])
  109. const example={pipes:[{code:'ISOLATED-SPLINE',industry:'GAS',diameter:300,positions:[[lon-.00012,lat],[lon-.00005,lat],[lon,lat+.00005]]}],components:[
  110. {code:'ISOLATED-ELBOW',kind:'elbow',industry:'GAS',position:[lon+.00004,lat]},
  111. {code:'ISOLATED-VALVE',kind:'valve',industry:'GAS',position:[lon+.00007,lat]},
  112. {code:'ISOLATED-TEE',kind:'branch',label:'tee',industry:'GAS',position:[lon+.0001,lat]},
  113. {code:'ISOLATED-CROSS',kind:'branch',label:'cross',industry:'GAS',position:[lon+.00013,lat]}]}
  114. const source=new CustomDataSource('pipe-isolated-acceptance')
  115. for(const row of example.components)source.entities.add({id:'component:'+row.code,position:Cartesian3.fromDegrees(...row.position),point:{pixelSize:14,color:Color.RED,heightReference:HeightReference.CLAMP_TO_GROUND,disableDepthTestDistance:Infinity},label:{text:row.kind+'(隔离样例)',heightReference:HeightReference.CLAMP_TO_GROUND,disableDepthTestDistance:Infinity}})
  116. await viewer.dataSources.add(source)
  117. const layer=createCesiumPipeModels(viewer,{source,onChange:s=>emit('examples',{isolated:true,missingKind,instances:s.instances,batches:s.batches,pending:s.pending,failed:s.failed,fallback:s.fallback,visibleSymbols:source.entities.values.filter(e=>e.show).length,byKind:s.byKind})})
  118. setViewMode('pipeline');applyViewPreset({lon,lat,height:ground.height},{heading:0,pitch:-45,range:60},{immediate:true})
  119. layer.sync(example)
  120. results.isolated=true
  121. })
  122. add('收起诊断',()=>panel.hidden=true)
  123. host.appendChild(panel)
  124. return {destroy:()=>panel.remove()}
  125. }