|
|
@@ -0,0 +1,126 @@
|
|
|
+// Temporary QA overlay mounted only for ?pipeVerify=1 on the shared cockpit route.
|
|
|
+// It exercises the actual scene; it is removed from mapService before delivery.
|
|
|
+import { Cartesian3, Cartographic, HeadingPitchRange, sampleTerrainMostDetailed } from 'cesium'
|
|
|
+import { toPipeNetwork } from '../../src/modules/cockpit/domain/pipeNetwork.js'
|
|
|
+import { pipeColorKey } from '../../src/modules/cockpit/domain/pipeVisual.js'
|
|
|
+
|
|
|
+export function mountPipeAcceptance({ viewer, host, setViewMode, applyViewPreset }) {
|
|
|
+ const params=new URLSearchParams(location.search)
|
|
|
+ const originalFetch=window.fetch.bind(window),missingKind=params.get('pipeMissing')
|
|
|
+ if (['valve','elbow','branch'].includes(missingKind)) {
|
|
|
+ window.fetch=(input,options)=>String(input).endsWith(`/models/pipes/${missingKind}.gltf`)
|
|
|
+ ? Promise.resolve(new Response('isolated missing template',{status:404})) : originalFetch(input,options)
|
|
|
+ }
|
|
|
+ const panel = document.createElement('aside')
|
|
|
+ panel.id = 'pipe-acceptance-overlay'
|
|
|
+ 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'
|
|
|
+ panel.innerHTML = '<b>管体验收(开发隔离控件)</b><div></div><pre style="white-space:pre-wrap"></pre>'
|
|
|
+ const buttons = panel.querySelector('div'), output = panel.querySelector('pre')
|
|
|
+ let network, target, range = 160, heading = 0
|
|
|
+ const results = {}
|
|
|
+ const emit = (key, value) => { results[key] = value; output.textContent = JSON.stringify(results, null, 2) }
|
|
|
+ const add = (name, action) => {
|
|
|
+ const button = document.createElement('button')
|
|
|
+ button.textContent = name; button.style.cssText = 'margin:3px;padding:5px;color:#fff;background:#12426a;border:1px solid #328abc'
|
|
|
+ button.onclick = () => Promise.resolve(action()).catch(error => emit('error', String(error.message || error)))
|
|
|
+ buttons.appendChild(button)
|
|
|
+ }
|
|
|
+ const pipes = async () => {
|
|
|
+ if (!network) {
|
|
|
+ const config = await fetch('/cockpit/config.json').then(r => r.json())
|
|
|
+ const payload = await fetch(config.api.baseUrl + '/infrastructure/geometry').then(r => r.json())
|
|
|
+ if (payload.code !== 200) throw new Error('geometry business error')
|
|
|
+ network = toPipeNetwork(payload.data)
|
|
|
+ }
|
|
|
+ return network
|
|
|
+ }
|
|
|
+ const aim = async (key) => {
|
|
|
+ const data = await pipes()
|
|
|
+ const row = key === 'MANHOLE' ? data.components.find(p => p.industry === 'MANHOLE') : data.pipes.find(p => p.diameter > 0 && pipeColorKey(p) === key)
|
|
|
+ if (!row) throw new Error('No real target ' + key)
|
|
|
+ const a = row.position || row.positions[0], b = row.position || row.positions.at(-1)
|
|
|
+ const lon = (a[0]+b[0])/2, lat = (a[1]+b[1])/2
|
|
|
+ const [point] = await sampleTerrainMostDetailed(viewer.terrainProvider, [Cartographic.fromDegrees(lon,lat)])
|
|
|
+ target = {code:row.code,key,lon,lat,height:point.height}
|
|
|
+ setViewMode('pipeline'); heading=0
|
|
|
+ applyViewPreset({lon,lat,height:point.height},{heading:0,pitch:-45,range},{immediate:true})
|
|
|
+ viewer.scene.requestRender()
|
|
|
+ emit('target', {...target,range,remark:row.remark||'',canvas:[viewer.canvas.width,viewer.canvas.height]})
|
|
|
+ }
|
|
|
+ const frame = () => new Promise(resolve => {
|
|
|
+ const detach = viewer.scene.postRender.addEventListener(() => { detach(); resolve() })
|
|
|
+ viewer.scene.requestRender()
|
|
|
+ })
|
|
|
+ const capture = () => {
|
|
|
+ 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
|
|
|
+ }
|
|
|
+ const modelList = () => {
|
|
|
+ const models=[]
|
|
|
+ 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)}
|
|
|
+ return models
|
|
|
+ }
|
|
|
+ add('真实污水近景',()=>aim('SEWAGE'))
|
|
|
+ add('真实雨水近景',()=>aim('RAINWATER'))
|
|
|
+ add('真实燃气近景',()=>aim('GAS'))
|
|
|
+ add('真实供水近景',()=>aim('WATER_SUPPLY'))
|
|
|
+ add('窨井紫色',()=>aim('MANHOLE'))
|
|
|
+ add('远景800m',async()=>{range=800;await aim(target?.key||'SEWAGE')})
|
|
|
+ add('近景160m',async()=>{range=160;await aim(target?.key||'SEWAGE')})
|
|
|
+ add('测管体像素',async()=>{
|
|
|
+ if(!target)throw new Error('Choose target')
|
|
|
+ const source=viewer.dataSources.getByName('cockpit-pipelines')[0],shown=source.show
|
|
|
+ const models=modelList(),states=models.map(m=>m.show)
|
|
|
+ source.show=false;models.forEach(m=>m.show=false)
|
|
|
+ try{
|
|
|
+ await frame();await frame();const before=capture()
|
|
|
+ models.forEach((m,i)=>m.show=states[i]);await frame();await frame();const after=capture()
|
|
|
+ const count={GAS:0,WATER_SUPPLY:0,SEWAGE:0,RAINWATER:0};let changed=0
|
|
|
+ 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++
|
|
|
+ if(r>120&&r>g*1.7&&r>b*1.35)count.GAS++
|
|
|
+ if(b>120&&b>r*1.7&&b>g*1.15)count.WATER_SUPPLY++
|
|
|
+ if(g>120&&g>r*1.7&&g>b*1.3)count.SEWAGE++
|
|
|
+ if(g>120&&b>120&&r<100&&Math.abs(g-b)<90)count.RAINWATER++}
|
|
|
+ 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]})
|
|
|
+ }finally{models.forEach((m,i)=>m.show=states[i]);source.show=shown;viewer.scene.requestRender()}
|
|
|
+ })
|
|
|
+ add('原生rAF',()=>{
|
|
|
+ const start=performance.now(),times=[]
|
|
|
+ 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()})}
|
|
|
+ requestAnimationFrame(next)
|
|
|
+ })
|
|
|
+ add('4K运动FPS',()=>{
|
|
|
+ if(!target)throw new Error('Choose target')
|
|
|
+ const scene=viewer.scene,old=scene.requestRenderMode,start=performance.now(),times=[];let hidden=false,lostFocus=false
|
|
|
+ scene.requestRenderMode=false
|
|
|
+ const detach=scene.postRender.addEventListener(()=>{
|
|
|
+ const now=performance.now();times.push(now);hidden ||= document.visibilityState!=='visible';lostFocus ||= !document.hasFocus()
|
|
|
+ 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}
|
|
|
+ detach();scene.requestRenderMode=old
|
|
|
+ const gaps=times.slice(1).map((t,i)=>t-times[i]).sort((a,b)=>a-b),secs=(times.at(-1)-times[0])/1000
|
|
|
+ const gl=viewer.canvas.getContext('webgl2')||viewer.canvas.getContext('webgl'),debug=gl.getExtension('WEBGL_debug_renderer_info')
|
|
|
+ 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})
|
|
|
+ })
|
|
|
+ })
|
|
|
+ add('隔离四类样例',async()=>{
|
|
|
+ const [{CustomDataSource,Color,HeightReference},{createCesiumPipeModels}]=await Promise.all([import('cesium'),import('../../src/modules/cockpit/services/cesiumPipeModels.js')])
|
|
|
+ const lon=110.39028,lat=28.44368
|
|
|
+ const [ground]=await sampleTerrainMostDetailed(viewer.terrainProvider,[Cartographic.fromDegrees(lon,lat)])
|
|
|
+ const example={pipes:[{code:'ISOLATED-SPLINE',industry:'GAS',diameter:300,positions:[[lon-.00012,lat],[lon-.00005,lat],[lon,lat+.00005]]}],components:[
|
|
|
+ {code:'ISOLATED-ELBOW',kind:'elbow',industry:'GAS',position:[lon+.00004,lat]},
|
|
|
+ {code:'ISOLATED-VALVE',kind:'valve',industry:'GAS',position:[lon+.00007,lat]},
|
|
|
+ {code:'ISOLATED-TEE',kind:'branch',label:'tee',industry:'GAS',position:[lon+.0001,lat]},
|
|
|
+ {code:'ISOLATED-CROSS',kind:'branch',label:'cross',industry:'GAS',position:[lon+.00013,lat]}]}
|
|
|
+ const source=new CustomDataSource('pipe-isolated-acceptance')
|
|
|
+ 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}})
|
|
|
+ await viewer.dataSources.add(source)
|
|
|
+ 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})})
|
|
|
+ setViewMode('pipeline');applyViewPreset({lon,lat,height:ground.height},{heading:0,pitch:-45,range:60},{immediate:true})
|
|
|
+ layer.sync(example)
|
|
|
+ results.isolated=true
|
|
|
+ })
|
|
|
+ add('收起诊断',()=>panel.hidden=true)
|
|
|
+ host.appendChild(panel)
|
|
|
+ return {destroy:()=>panel.remove()}
|
|
|
+}
|