// 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 = '管体验收(开发隔离控件)
'
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;iaim('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;i120&&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()}
}